Merge branch 'TIF/Runtime' into TIF/Jenkins

This commit is contained in:
jonawals
2021-06-08 14:27:18 +01:00
718 changed files with 26957 additions and 17896 deletions
+2 -2
View File
@@ -125,8 +125,8 @@ ly_add_target(
AZ::AssetProcessorBatch.Static
)
if(LY_FIRST_PROJECT_PATH)
set_property(TARGET AssetProcessor AssetProcessorBatch APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_FIRST_PROJECT_PATH}\"")
if(LY_DEFAULT_PROJECT_PATH)
set_property(TARGET AssetProcessor AssetProcessorBatch APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_DEFAULT_PROJECT_PATH}\"")
endif()
# Adds the AssetProcessorBatch target as a C preprocessor define so that it can be used as a Settings Registry
@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.02554 10L9 15.0362V18L0 9L9 0V2.98885L4 8H18V10H4.02554Z" fill="#1e70eb"/>
</svg>

After

Width:  |  Height:  |  Size: 230 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 17L15 14H9L12 17Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f82f22df64b93d4bec91e56b60efa3d5ce2915ce388a2dc627f1ab720678e3d5
size 334987
oid sha256:4a5881b8d6cfbc4ceefb14ab96844484fe19407ee030824768f9fcce2f729d35
size 2949
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8358f4dad9878c662b9819b2b346622af691eb45f8eddc28fff79a50650ae6cf
size 2503
@@ -7,6 +7,7 @@
<file>AddOffset.svg</file>
<file>AddOffset_Hover.svg</file>
<file>ArrowBack.svg</file>
<file>ArrowBack_Hover.svg</file>
<file>build.svg</file>
<file>FolderOffset.svg</file>
<file>FolderOffset_Hover.svg</file>
@@ -18,6 +19,7 @@
<file>Linux.svg</file>
<file>macOS.svg</file>
<file>DefaultProjectImage.png</file>
<file>DefaultTemplate.png</file>
<file>ArrowDownLine.svg</file>
<file>ArrowUpLine.svg</file>
<file>o3de.svg</file>
@@ -26,5 +28,8 @@
<file>Backgrounds/FirstTimeBackgroundImage.jpg</file>
<file>ArrowDownLine.svg</file>
<file>ArrowUpLine.svg</file>
<file>CarrotArrowDown.svg</file>
<file>Summary.svg</file>
<file>WindowClose.svg</file>
</qresource>
</RCC>
@@ -115,7 +115,7 @@ QTabBar::tab:pressed
/************** General (Modal windows) **************/
#header {
background-color:#111111;
background-color:#111111;
min-height:80px;
max-height:80px;
}
@@ -136,11 +136,9 @@ QTabBar::tab:pressed
#header QPushButton:focus {
border:none;
}
#header QPushButton:hover {
background:#333333 url(:/ArrowBack.svg) no-repeat center;
}
#header QPushButton:hover,
#header QPushButton:pressed {
background:#222222 url(:/ArrowBack.svg) no-repeat center;
background:transparent url(:/ArrowBack_Hover.svg) no-repeat center;
}
#headerTitle {
@@ -172,8 +170,8 @@ QTabBar::tab:pressed
#footer > QPushButton {
qproperty-flat: true;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0095f2, stop: 1.0 #1e70eb);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0095f2, stop: 1.0 #1e70eb);
border-radius: 3px;
min-height: 28px;
max-height: 28px;
@@ -181,26 +179,26 @@ QTabBar::tab:pressed
margin-right:30px;
}
#footer > QPushButton:hover {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #10A5f2, stop: 1.0 #2e80eb);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #10A5f2, stop: 1.0 #2e80eb);
}
#footer > QPushButton:pressed {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0085e2, stop: 1.0 #0e60db);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0085e2, stop: 1.0 #0e60db);
}
#footer > QPushButton[secondary="true"] {
margin-right: 10px;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #888888, stop: 1.0 #555555);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #888888, stop: 1.0 #555555);
}
#footer > QPushButton[secondary="true"]:hover {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #999999, stop: 1.0 #666666);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #999999, stop: 1.0 #666666);
}
#footer > QPushButton[secondary="true"]:pressed {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #555555, stop: 1.0 #777777);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #555555, stop: 1.0 #777777);
}
/************** Project Settings **************/
@@ -210,9 +208,6 @@ QTabBar::tab:pressed
#projectTemplate {
margin: 55px 0 0 50px;
max-width: 780px;
min-height:200px;
max-height:200px;
}
#projectTemplateLabel {
font-size:16px;
@@ -227,11 +222,67 @@ QTabBar::tab:pressed
#projectTemplateDetails {
background-color:#444444;
max-width:240px;
max-width:20%;
min-width:240px;
margin-left:30px;
}
#projectTemplateDetails #displayName,
#projectTemplateDetails #includedGemsTitle {
font-size:18px;
}
#projectTemplateDetails #moreGems {
font-size:14px;
margin-top:20px;
}
#projectTemplateDetails #includedGemsTitle {
margin-top:5px;
margin-bottom:5px;
}
#projectTemplateDetails #summary {
padding-bottom:0px;
border-bottom:2px solid #555555;
min-height:80px;
qproperty-alignment: AlignTop;
}
#projectTemplateDetails #browseCatalog {
margin:5px 0px 15px 0px;
}
#projectTemplate QPushButton {
qproperty-flat: true;
min-width: 96px;
max-width: 96px;
min-height: 160px;
max-height: 160px;
}
#projectTemplate #templateLabel {
qproperty-alignment: AlignCenter;
}
#projectTemplate QPushButton #templateImage {
border:3px solid transparent;
border-radius: 4px;
}
#projectTemplate QPushButton[Checked="true"] #templateImage {
border:3px solid #1e70eb;
}
#projectTemplate QPushButton[Checked="true"] #templateLabel {
font-weight:bold;
}
#projectTemplate QPushButton:hover {
background-color: #444444;
}
#projectTemplate QPushButton:focus {
outline: none;
border:none;
}
#projectSettingsTab::tab-bar {
left: 60px;
}
@@ -240,6 +291,30 @@ QTabBar::tab:pressed
height:50px;
}
#projectSettingsTab::tab-bar > QPushButton {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #888888, stop: 1.0 #555555);
qproperty-flat: true;
margin-right:30px;
margin-bottom:12px;
margin-top:0px;
min-width:170px;
max-width:170px;
min-height:26px;
max-height:26px;
border-radius: 3px;
text-align:center;
font-size:13px;
}
#projectSettingsTab::tab-bar > QPushButton:hover {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #999999, stop: 1.0 #666666);
}
#projectSettingsTab::tab-bar > QPushButton:pressed {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #555555, stop: 1.0 #777777);
}
#projectSettingsTopFrame {
background-color:#1E252F;
}
@@ -311,6 +386,7 @@ QTabBar::tab:pressed
#projectButton > #labelButton {
border:1px solid white;
}
#projectButton > #labelButton:hover,
#projectButton > #labelButton:pressed {
border:1px solid #1e70eb;
@@ -350,12 +426,38 @@ QTabBar::tab:pressed
max-height:278px;
}
QProgressBar {
border: none;
background-color: transparent;
padding: 0px;
min-height: 14px;
font-size: 2px;
}
QProgressBar::chunk {
background-color: #1E70EB;
}
/************** Gem Catalog **************/
#GemCatalogTitle {
font-size: 18px;
}
#GemCatalogCart {
background-color: #555555;
}
#GemCatalogCartCountLabel {
font-size: 12px;
background-color: #4285F4;
border-radius: 3px;
}
#GemCatalogCartOverlaySectionLabel {
font-weight: 600;
}
/************** Gem Catalog (Inspector) **************/
#GemCatalogInspector {
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5714 6.7619H16.7619C16.7619 4.13333 14.6286 2 12 2C9.37145 2 7.23811 4.13333 7.23811 6.7619H3.42859V22H20.5714V6.7619ZM12 3.90476C13.581 3.90476 14.8572 5.18095 14.8572 6.7619H9.14287C9.14287 5.18095 10.4191 3.90476 12 3.90476ZM13.7997 11.7424L12.0288 9.24095H11.9712L10.2003 11.7424H13.7997ZM5.00002 12.4335L10.9158 19.4063L9.03253 12.4335H5.00002ZM10.14 12.4335L11.9356 19.5147C11.9548 19.5189 11.974 19.521 11.9959 19.521C12.0178 19.521 12.0398 19.5189 12.0617 19.5147L13.86 12.4335H10.14ZM14.9675 12.4335H18.8547H19L13.0842 19.398L14.9675 12.4335ZM8.26457 9.24095H11.075L9.52787 11.4253L8.26457 9.24095ZM8.83789 11.7424L7.5193 9.46191L5.01921 11.7424H5.14804H8.83789ZM18.9808 11.7424H18.8547H15.1512L16.4286 9.41397L18.9808 11.7424ZM15.6558 9.24095H12.9252L14.4647 11.4146L15.6558 9.24095Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 966 B

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5507 19.5506L19.5213 2.58008L21.289 4.34785L4.31846 21.3184L2.5507 19.5506Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.28706 2.65015L21.2576 19.6207L19.4899 21.3885L2.51929 4.41791L4.28706 2.65015Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

@@ -15,6 +15,7 @@
#include <PythonBindingsInterface.h>
#include <NewProjectSettingsScreen.h>
#include <ScreenHeaderWidget.h>
#include <GemCatalog/GemCatalogScreen.h>
#include <QDialogButtonBox>
#include <QHBoxLayout>
@@ -41,24 +42,33 @@ namespace O3DE::ProjectManager
m_stack = new QStackedWidget(this);
m_stack->setObjectName("body");
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding));
m_stack->addWidget(new NewProjectSettingsScreen());
m_gemCatalog = new GemCatalogScreen();
m_stack->addWidget(m_gemCatalog);
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred,QSizePolicy::Expanding));
m_newProjectSettingsScreen = new NewProjectSettingsScreen(this);
m_stack->addWidget(m_newProjectSettingsScreen);
m_gemCatalogScreen = new GemCatalogScreen(this);
m_stack->addWidget(m_gemCatalogScreen);
vLayout->addWidget(m_stack);
QDialogButtonBox* backNextButtons = new QDialogButtonBox();
backNextButtons->setObjectName("footer");
vLayout->addWidget(backNextButtons);
QDialogButtonBox* buttons = new QDialogButtonBox();
buttons->setObjectName("footer");
vLayout->addWidget(buttons);
m_backButton = backNextButtons->addButton(tr("Back"), QDialogButtonBox::RejectRole);
m_backButton->setProperty("secondary", true);
m_nextButton = backNextButtons->addButton(tr("Next"), QDialogButtonBox::ApplyRole);
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
connect(m_newProjectSettingsScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest);
connect(m_backButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandleBackButton);
connect(m_nextButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandleNextButton);
m_secondaryButton = buttons->addButton(tr("Back"), QDialogButtonBox::RejectRole);
m_secondaryButton->setProperty("secondary", true);
m_secondaryButton->setVisible(false);
connect(m_secondaryButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandleSecondaryButton);
Update();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
m_primaryButton = buttons->addButton(tr("Create Project"), QDialogButtonBox::ApplyRole);
connect(m_primaryButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandlePrimaryButton);
setLayout(vLayout);
}
@@ -80,8 +90,10 @@ namespace O3DE::ProjectManager
{
if (m_stack->currentIndex() > 0)
{
m_stack->setCurrentIndex(m_stack->currentIndex() - 1);
Update();
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
PreviousScreen();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
}
else
{
@@ -89,70 +101,123 @@ namespace O3DE::ProjectManager
}
}
void CreateProjectCtrl::HandleNextButton()
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
void CreateProjectCtrl::HandleSecondaryButton()
{
ScreenWidget* currentScreen = reinterpret_cast<ScreenWidget*>(m_stack->currentWidget());
ProjectManagerScreen screenEnum = currentScreen->GetScreenEnum();
if (screenEnum == ProjectManagerScreen::NewProjectSettings)
if (m_stack->currentIndex() > 0)
{
auto newProjectScreen = reinterpret_cast<NewProjectSettingsScreen*>(currentScreen);
if (newProjectScreen)
{
if (!newProjectScreen->Validate())
{
QMessageBox::critical(this, tr("Invalid project settings"), tr("Invalid project settings"));
return;
}
m_projectInfo = newProjectScreen->GetProjectInfo();
m_projectTemplatePath = newProjectScreen->GetProjectTemplatePath();
// The next page is the gem catalog. Gather the available gems that will be shown in the gem catalog.
m_gemCatalog->ReinitForProject(m_projectInfo.m_path, /*isNewProject=*/true);
}
}
if (m_stack->currentIndex() != m_stack->count() - 1)
{
m_stack->setCurrentIndex(m_stack->currentIndex() + 1);
Update();
// return to Project Settings page
PreviousScreen();
}
else
{
auto result = PythonBindingsInterface::Get()->CreateProject(m_projectTemplatePath, m_projectInfo);
// Configure Gems
NextScreen();
}
}
void CreateProjectCtrl::Update()
{
if (m_stack->currentWidget() == m_gemCatalogScreen)
{
m_header->setSubTitle(tr("Configure project with Gems"));
m_secondaryButton->setVisible(false);
}
else
{
m_header->setSubTitle(tr("Enter Project Details"));
m_secondaryButton->setVisible(true);
m_secondaryButton->setText(tr("Configure Gems"));
}
}
void CreateProjectCtrl::OnChangeScreenRequest(ProjectManagerScreen screen)
{
if (screen == ProjectManagerScreen::GemCatalog)
{
HandleSecondaryButton();
}
else
{
emit ChangeScreenRequest(screen);
}
}
void CreateProjectCtrl::NextScreen()
{
if (m_stack->currentIndex() < m_stack->count())
{
if(CurrentScreenIsValid())
{
m_stack->setCurrentIndex(m_stack->currentIndex() + 1);
QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
m_gemCatalogScreen->ReinitForProject(projectTemplatePath + "/Template", /*isNewProject=*/true);
Update();
}
else
{
QMessageBox::warning(this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
}
}
}
void CreateProjectCtrl::PreviousScreen()
{
// we don't require the current screen to be valid when moving back
if (m_stack->currentIndex() > 0)
{
m_stack->setCurrentIndex(m_stack->currentIndex() - 1);
Update();
}
}
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
void CreateProjectCtrl::HandlePrimaryButton()
{
CreateProject();
}
bool CreateProjectCtrl::CurrentScreenIsValid()
{
if (m_stack->currentWidget() == m_newProjectSettingsScreen)
{
return m_newProjectSettingsScreen->Validate();
}
return true;
}
void CreateProjectCtrl::CreateProject()
{
if (m_newProjectSettingsScreen->Validate())
{
ProjectInfo projectInfo = m_newProjectSettingsScreen->GetProjectInfo();
QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
auto result = PythonBindingsInterface::Get()->CreateProject(projectTemplatePath, projectInfo);
if (result.IsSuccess())
{
// automatically register the project
PythonBindingsInterface::Get()->AddProject(m_projectInfo.m_path);
PythonBindingsInterface::Get()->AddProject(projectInfo.m_path);
// adding gems is not implemented yet because we don't know what targets to add or how to add them
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
m_gemCatalogScreen->EnableDisableGemsForProject(projectInfo.m_path);
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
projectInfo.m_needsBuild = true;
emit NotifyBuildProject(projectInfo);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
else
{
QMessageBox::critical(this, tr("Project creation failed"), tr("Failed to create project."));
}
// Enable/disable gems for the newly created project.
m_gemCatalog->EnableDisableGemsForProject(m_projectInfo.m_path);
}
}
void CreateProjectCtrl::Update()
{
ScreenWidget* currentScreen = reinterpret_cast<ScreenWidget*>(m_stack->currentWidget());
if (currentScreen && currentScreen->GetScreenEnum() == ProjectManagerScreen::GemCatalog)
{
m_header->setTitle(tr("Create Project"));
m_header->setSubTitle(tr("Configure project with Gems"));
m_nextButton->setText(tr("Create Project"));
}
else
{
m_header->setTitle(tr("Create Project"));
m_header->setSubTitle(tr("Enter Project Details"));
m_nextButton->setText(tr("Next"));
QMessageBox::warning(this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
}
}
@@ -14,9 +14,11 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <ProjectInfo.h>
#include <GemCatalog/GemCatalogScreen.h>
#endif
// due to current limitations, customizing template Gems is disabled
#define TEMPLATE_GEM_CONFIGURATION_ENABLED
QT_FORWARD_DECLARE_CLASS(QStackedWidget)
QT_FORWARD_DECLARE_CLASS(QPushButton)
QT_FORWARD_DECLARE_CLASS(QLabel)
@@ -24,6 +26,8 @@ QT_FORWARD_DECLARE_CLASS(QLabel)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(ScreenHeader)
QT_FORWARD_DECLARE_CLASS(NewProjectSettingsScreen)
QT_FORWARD_DECLARE_CLASS(GemCatalogScreen)
class CreateProjectCtrl
: public ScreenWidget
@@ -36,21 +40,37 @@ namespace O3DE::ProjectManager
protected slots:
void HandleBackButton();
void HandleNextButton();
void HandlePrimaryButton();
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
void OnChangeScreenRequest(ProjectManagerScreen screen);
void HandleSecondaryButton();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
private:
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
void Update();
void NextScreen();
void PreviousScreen();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
QStackedWidget* m_stack;
ScreenHeader* m_header;
bool CurrentScreenIsValid();
void CreateProject();
QPushButton* m_backButton;
QPushButton* m_nextButton;
QStackedWidget* m_stack = nullptr;
ScreenHeader* m_header = nullptr;
QPushButton* m_primaryButton = nullptr;
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
QPushButton* m_secondaryButton = nullptr;
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
QString m_projectTemplatePath;
ProjectInfo m_projectInfo;
GemCatalogScreen* m_gemCatalog = nullptr;
NewProjectSettingsScreen* m_newProjectSettingsScreen = nullptr;
GemCatalogScreen* m_gemCatalogScreen = nullptr;
};
} // namespace O3DE::ProjectManager
@@ -10,24 +10,229 @@
*
*/
#include <AzQtComponents/Components/SearchLineEdit.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QLabel>
#include <QPushButton>
#include <TagWidget.h>
namespace O3DE::ProjectManager
{
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent)
CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
{
setObjectName("GemCatalogCart");
m_layout = new QVBoxLayout();
m_layout->setSpacing(0);
m_layout->setMargin(0);
m_layout->setAlignment(Qt::AlignTop);
setLayout(m_layout);
QHBoxLayout* hLayout = new QHBoxLayout();
QPushButton* closeButton = new QPushButton();
closeButton->setFlat(true);
closeButton->setFocusPolicy(Qt::NoFocus);
closeButton->setIcon(QIcon(":/WindowClose.svg"));
connect(closeButton, &QPushButton::clicked, this, [=]
{
deleteLater();
});
hLayout->addSpacerItem(new QSpacerItem(10, 0, QSizePolicy::Expanding));
hLayout->addWidget(closeButton);
m_layout->addLayout(hLayout);
// enabled
{
m_enabledWidget = new QWidget();
m_enabledWidget->setFixedWidth(s_width);
m_layout->addWidget(m_enabledWidget);
QVBoxLayout* layout = new QVBoxLayout();
layout->setAlignment(Qt::AlignTop);
m_enabledWidget->setLayout(layout);
m_enabledLabel = new QLabel();
m_enabledLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
layout->addWidget(m_enabledLabel);
m_enabledTagContainer = new TagContainerWidget();
layout->addWidget(m_enabledTagContainer);
}
// disabled
{
m_disabledWidget = new QWidget();
m_disabledWidget->setFixedWidth(s_width);
m_layout->addWidget(m_disabledWidget);
QVBoxLayout* layout = new QVBoxLayout();
layout->setAlignment(Qt::AlignTop);
m_disabledWidget->setLayout(layout);
m_disabledLabel = new QLabel();
m_disabledLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
layout->addWidget(m_disabledLabel);
m_disabledTagContainer = new TagContainerWidget();
layout->addWidget(m_disabledTagContainer);
}
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
Update();
connect(gemModel, &GemModel::dataChanged, this, [=]
{
Update();
});
}
void CartOverlayWidget::Update()
{
const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
if (toBeAdded.isEmpty())
{
m_enabledWidget->hide();
}
else
{
m_enabledTagContainer->Update(ConvertFromModelIndices(toBeAdded));
m_enabledLabel->setText(QString("%1 %2").arg(QString::number(toBeAdded.size()), tr("Gems to be enabled")));
m_enabledWidget->show();
}
const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
if (toBeRemoved.isEmpty())
{
m_disabledWidget->hide();
}
else
{
m_disabledTagContainer->Update(ConvertFromModelIndices(toBeRemoved));
m_disabledLabel->setText(QString("%1 %2").arg(QString::number(toBeRemoved.size()), tr("Gems to be disabled")));
m_disabledWidget->show();
}
}
QStringList CartOverlayWidget::ConvertFromModelIndices(const QVector<QModelIndex>& gems) const
{
QStringList gemNames;
gemNames.reserve(gems.size());
for (const QModelIndex& modelIndex : gems)
{
gemNames.push_back(GemModel::GetName(modelIndex));
}
return gemNames;
}
CartButton::CartButton(GemModel* gemModel, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
{
m_layout = new QHBoxLayout();
m_layout->setMargin(0);
setLayout(m_layout);
QPushButton* iconButton = new QPushButton();
iconButton->setFlat(true);
iconButton->setFocusPolicy(Qt::NoFocus);
iconButton->setIcon(QIcon(":/Summary.svg"));
iconButton->setFixedSize(s_iconSize, s_iconSize);
connect(iconButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
m_layout->addWidget(iconButton);
m_countLabel = new QLabel();
m_countLabel->setObjectName("GemCatalogCartCountLabel");
m_countLabel->setFixedHeight(s_iconSize - 1); // Compensate for the empty icon space by using a slightly smaller label height.
m_layout->addWidget(m_countLabel);
m_dropDownButton = new QPushButton();
m_dropDownButton->setFlat(true);
m_dropDownButton->setFocusPolicy(Qt::NoFocus);
m_dropDownButton->setIcon(QIcon(":/CarrotArrowDown.svg"));
m_dropDownButton->setFixedSize(s_arrowDownIconSize, s_arrowDownIconSize);
connect(m_dropDownButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
m_layout->addWidget(m_dropDownButton);
// Adjust the label text whenever the model gets updated.
connect(gemModel, &GemModel::dataChanged, [=]
{
const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
const int count = toBeAdded.size() + toBeRemoved.size();
m_countLabel->setText(QString::number(count));
m_dropDownButton->setVisible(!toBeAdded.isEmpty() || !toBeRemoved.isEmpty());
// Automatically close the overlay window in case there are no gems to be enabled or disabled anymore.
if (m_cartOverlay && toBeAdded.isEmpty() && toBeRemoved.isEmpty())
{
m_cartOverlay->deleteLater();
m_cartOverlay = nullptr;
}
});
}
void CartButton::mousePressEvent([[maybe_unused]] QMouseEvent* event)
{
ShowOverlay();
}
void CartButton::ShowOverlay()
{
const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
if (toBeAdded.isEmpty() && toBeRemoved.isEmpty())
{
return;
}
if (m_cartOverlay)
{
// Directly delete the former overlay before creating the new one.
// Don't use deleteLater() here. This might overwrite the new overlay pointer
// depending on the event queue.
delete m_cartOverlay;
}
m_cartOverlay = new CartOverlayWidget(m_gemModel, this);
connect(m_cartOverlay, &QWidget::destroyed, this, [=]
{
// Reset the overlay pointer on destruction to prevent dangling pointers.
m_cartOverlay = nullptr;
});
m_cartOverlay->show();
const QPoint parentPos = m_dropDownButton->mapToParent(m_dropDownButton->pos());
const QPoint globalPos = m_dropDownButton->mapToGlobal(m_dropDownButton->pos());
const QPoint offset(-4, 10);
m_cartOverlay->setGeometry(globalPos.x() - parentPos.x() - m_cartOverlay->width() + width() + offset.x(),
globalPos.y() + offset.y(),
m_cartOverlay->width(),
m_cartOverlay->height());
}
CartButton::~CartButton()
{
// Make sure the overlay window is automatically closed in case the gem catalog is destroyed.
if (m_cartOverlay)
{
m_cartOverlay->deleteLater();
}
}
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent)
: QFrame(parent)
{
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setAlignment(Qt::AlignLeft);
hLayout->setMargin(0);
hLayout->setContentsMargins(10, 7, 10, 7);
setLayout(hLayout);
setObjectName("GemCatalogHeaderWidget");
hLayout->addSpacing(7);
setFixedHeight(s_height);
QLabel* titleLabel = new QLabel(tr("Gem Catalog"));
titleLabel->setObjectName("GemCatalogTitle");
@@ -35,17 +240,23 @@ namespace O3DE::ProjectManager
hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding));
AzQtComponents::SearchLineEdit* filterLineEdit = new AzQtComponents::SearchLineEdit();
filterLineEdit->setStyleSheet("background-color: #DDDDDD;");
connect(filterLineEdit, &QLineEdit::textChanged, this, [=](const QString& text)
m_filterLineEdit = new AzQtComponents::SearchLineEdit();
m_filterLineEdit->setStyleSheet("background-color: #DDDDDD;");
connect(m_filterLineEdit, &QLineEdit::textChanged, this, [=](const QString& text)
{
filterProxyModel->SetSearchString(text);
});
hLayout->addWidget(filterLineEdit);
hLayout->addWidget(m_filterLineEdit);
hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding));
hLayout->addSpacerItem(new QSpacerItem(140, 0, QSizePolicy::Fixed));
setFixedHeight(60);
hLayout->addSpacerItem(new QSpacerItem(75, 0, QSizePolicy::Fixed));
CartButton* cartButton = new CartButton(gemModel);
hLayout->addWidget(cartButton);
}
void GemCatalogHeaderWidget::ReinitForProject()
{
m_filterLineEdit->setText({});
}
} // namespace O3DE::ProjectManager
@@ -13,19 +13,81 @@
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzQtComponents/Components/SearchLineEdit.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <TagWidget.h>
#include <QFrame>
#include <QLabel>
#include <QDialog>
#include <QMoveEvent>
#include <QVBoxLayout>
#endif
namespace O3DE::ProjectManager
{
class CartOverlayWidget
: public QWidget
{
Q_OBJECT // AUTOMOC
public:
CartOverlayWidget(GemModel* gemModel, QWidget* parent = nullptr);
void Update();
private:
QStringList ConvertFromModelIndices(const QVector<QModelIndex>& gems) const;
QVBoxLayout* m_layout = nullptr;
GemModel* m_gemModel = nullptr;
QWidget* m_enabledWidget = nullptr;
QLabel* m_enabledLabel = nullptr;
TagContainerWidget* m_enabledTagContainer = nullptr;
QWidget* m_disabledWidget = nullptr;
QLabel* m_disabledLabel = nullptr;
TagContainerWidget* m_disabledTagContainer = nullptr;
inline constexpr static int s_width = 240;
};
class CartButton
: public QWidget
{
Q_OBJECT // AUTOMOC
public:
CartButton(GemModel* gemModel, QWidget* parent = nullptr);
~CartButton();
void ShowOverlay();
private:
void mousePressEvent(QMouseEvent* event) override;
GemModel* m_gemModel = nullptr;
QHBoxLayout* m_layout = nullptr;
QLabel* m_countLabel = nullptr;
QPushButton* m_dropDownButton = nullptr;
CartOverlayWidget* m_cartOverlay = nullptr;
inline constexpr static int s_iconSize = 24;
inline constexpr static int s_arrowDownIconSize = 8;
};
class GemCatalogHeaderWidget
: public QFrame
{
Q_OBJECT // AUTOMOC
public:
explicit GemCatalogHeaderWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr);
explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr);
~GemCatalogHeaderWidget() = default;
void ReinitForProject();
private:
AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr;
inline constexpr static int s_height = 60;
};
} // namespace O3DE::ProjectManager
@@ -12,7 +12,6 @@
#include <GemCatalog/GemCatalogScreen.h>
#include <PythonBindingsInterface.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemListHeaderWidget.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <QVBoxLayout>
@@ -35,8 +34,8 @@ namespace O3DE::ProjectManager
vLayout->setSpacing(0);
setLayout(vLayout);
GemCatalogHeaderWidget* headerWidget = new GemCatalogHeaderWidget(m_proxModel);
vLayout->addWidget(headerWidget);
m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel);
vLayout->addWidget(m_headerWidget);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setMargin(0);
@@ -77,10 +76,11 @@ namespace O3DE::ProjectManager
m_filterWidget->deleteLater();
}
m_proxModel->ResetFilters();
m_filterWidget = new GemFilterWidget(m_proxModel);
m_filterWidgetLayout->addWidget(m_filterWidget);
m_proxModel->InvalidateFilter();
m_headerWidget->ReinitForProject();
// Select the first entry after everything got correctly sized
QTimer::singleShot(200, [=]{
@@ -14,6 +14,7 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemFilterWidget.h>
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemInspector.h>
@@ -40,6 +41,7 @@ namespace O3DE::ProjectManager
GemListView* m_gemListView = nullptr;
GemInspector* m_gemInspector = nullptr;
GemModel* m_gemModel = nullptr;
GemCatalogHeaderWidget* m_headerWidget = nullptr;
GemSortFilterProxyModel* m_proxModel = nullptr;
QVBoxLayout* m_filterWidgetLayout = nullptr;
GemFilterWidget* m_filterWidget = nullptr;
@@ -53,6 +53,7 @@ namespace O3DE::ProjectManager
QFont standardFont(options.font);
standardFont.setPixelSize(s_fontSize);
QFontMetrics standardFontMetrics(standardFont);
painter->save();
painter->setClipping(true);
@@ -78,8 +79,10 @@ namespace O3DE::ProjectManager
}
// Gem name
const QString gemName = GemModel::GetName(modelIndex);
QString gemName = GemModel::GetName(modelIndex);
QFont gemNameFont(options.font);
const int firstColumnMaxTextWidth = s_summaryStartX - 30;
gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
gemNameFont.setPixelSize(s_gemNameFontSize);
gemNameFont.setBold(true);
QRect gemNameRect = GetTextRect(gemNameFont, gemName, s_gemNameFontSize);
@@ -90,7 +93,8 @@ namespace O3DE::ProjectManager
painter->drawText(gemNameRect, Qt::TextSingleLine, gemName);
// Gem creator
const QString gemCreator = GemModel::GetCreator(modelIndex);
QString gemCreator = GemModel::GetCreator(modelIndex);
gemCreator = standardFontMetrics.elidedText(gemCreator, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
QRect gemCreatorRect = GetTextRect(standardFont, gemCreator, s_fontSize);
gemCreatorRect.moveTo(contentRect.left(), contentRect.top() + gemNameRect.height());
@@ -130,4 +130,15 @@ namespace O3DE::ProjectManager
invalidate();
emit OnInvalidated();
}
void GemSortFilterProxyModel::ResetFilters()
{
m_searchString.clear();
m_gemOriginFilter = {};
m_platformFilter = {};
m_typeFilter = {};
m_featureFilter = {};
InvalidateFilter();
}
} // namespace O3DE::ProjectManager
@@ -51,6 +51,7 @@ namespace O3DE::ProjectManager
void SetFeatures(const QSet<QString>& features) { m_featureFilter = features; InvalidateFilter(); }
void InvalidateFilter();
void ResetFilters();
signals:
void OnInvalidated();
@@ -14,8 +14,12 @@
#include <PythonBindingsInterface.h>
#include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h>
#include <TemplateButtonWidget.h>
#include <PathValidator.h>
#include <EngineInfo.h>
#include <CreateProjectCtrl.h>
#include <TagWidget.h>
#include <AzQtComponents/Components/FlowLayout.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -28,10 +32,12 @@
#include <QSpacerItem>
#include <QStandardPaths>
#include <QFrame>
#include <QScrollArea>
#include <QAbstractButton>
namespace O3DE::ProjectManager
{
constexpr const char* k_pathProperty = "Path";
constexpr const char* k_templateIndexProperty = "TemplateIndex";
NewProjectSettingsScreen::NewProjectSettingsScreen(QWidget* parent)
: ProjectSettingsScreen(parent)
@@ -59,30 +65,69 @@ namespace O3DE::ProjectManager
projectTemplateDetailsLabel->setObjectName("projectTemplateDetailsLabel");
containerLayout->addWidget(projectTemplateDetailsLabel);
QHBoxLayout* templateLayout = new QHBoxLayout(this);
containerLayout->addItem(templateLayout);
// we might have enough templates that we need to scroll
QScrollArea* templatesScrollArea = new QScrollArea(this);
QWidget* scrollWidget = new QWidget();
FlowLayout* flowLayout = new FlowLayout(0, s_spacerSize, s_spacerSize);
scrollWidget->setLayout(flowLayout);
templatesScrollArea->setWidget(scrollWidget);
templatesScrollArea->setWidgetResizable(true);
m_projectTemplateButtonGroup = new QButtonGroup(this);
m_projectTemplateButtonGroup->setObjectName("templateButtonGroup");
// QButtonGroup has overloaded buttonClicked methods so we need the QOverload
connect(
m_projectTemplateButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this,
[=](QAbstractButton* button)
{
if (button && button->property(k_templateIndexProperty).isValid())
{
int projectIndex = button->property(k_templateIndexProperty).toInt();
UpdateTemplateDetails(m_templates.at(projectIndex));
}
});
auto templatesResult = PythonBindingsInterface::Get()->GetProjectTemplates();
if (templatesResult.IsSuccess() && !templatesResult.GetValue().isEmpty())
{
for (const ProjectTemplateInfo& projectTemplate : templatesResult.GetValue())
{
QRadioButton* radioButton = new QRadioButton(projectTemplate.m_name, this);
radioButton->setProperty(k_pathProperty, projectTemplate.m_path);
m_projectTemplateButtonGroup->addButton(radioButton);
m_templates = templatesResult.GetValue();
containerLayout->addWidget(radioButton);
// sort alphabetically by display name because they could be in any order
std::sort(m_templates.begin(), m_templates.end(), [](const ProjectTemplateInfo& arg1, const ProjectTemplateInfo& arg2)
{
return arg1.m_displayName.toLower() < arg2.m_displayName.toLower();
});
for (int index = 0; index < m_templates.size(); ++index)
{
ProjectTemplateInfo projectTemplate = m_templates.at(index);
QString projectPreviewPath = projectTemplate.m_path + "/Template/preview.png";
QFileInfo doesPreviewExist(projectPreviewPath);
if (!doesPreviewExist.exists() || !doesPreviewExist.isFile())
{
projectPreviewPath = ":/DefaultTemplate.png";
}
TemplateButton* templateButton = new TemplateButton(projectPreviewPath, projectTemplate.m_displayName, this);
templateButton->setCheckable(true);
templateButton->setProperty(k_templateIndexProperty, index);
m_projectTemplateButtonGroup->addButton(templateButton);
flowLayout->addWidget(templateButton);
}
m_projectTemplateButtonGroup->buttons().first()->setChecked(true);
}
containerLayout->addWidget(templatesScrollArea);
}
projectTemplateWidget->setLayout(containerLayout);
m_verticalLayout->addWidget(projectTemplateWidget);
QWidget* projectTemplateDetails = new QWidget(this);
QFrame* projectTemplateDetails = CreateTemplateDetails(s_templateDetailsContentMargin);
projectTemplateDetails->setObjectName("projectTemplateDetails");
m_horizontalLayout->addWidget(projectTemplateDetails);
}
@@ -109,11 +154,71 @@ namespace O3DE::ProjectManager
void NewProjectSettingsScreen::NotifyCurrentScreen()
{
if (!m_templates.isEmpty())
{
UpdateTemplateDetails(m_templates.first());
}
Validate();
}
QString NewProjectSettingsScreen::GetProjectTemplatePath()
{
return m_projectTemplateButtonGroup->checkedButton()->property(k_pathProperty).toString();
const int templateIndex = m_projectTemplateButtonGroup->checkedButton()->property(k_templateIndexProperty).toInt();
return m_templates.at(templateIndex).m_path;
}
QFrame* NewProjectSettingsScreen::CreateTemplateDetails(int margin)
{
QFrame* projectTemplateDetails = new QFrame(this);
projectTemplateDetails->setObjectName("projectTemplateDetails");
QVBoxLayout* templateDetailsLayout = new QVBoxLayout();
templateDetailsLayout->setContentsMargins(margin, margin, margin, margin);
templateDetailsLayout->setAlignment(Qt::AlignTop);
{
m_templateDisplayName = new QLabel(this);
m_templateDisplayName->setObjectName("displayName");
templateDetailsLayout->addWidget(m_templateDisplayName);
m_templateSummary = new QLabel(this);
m_templateSummary->setObjectName("summary");
m_templateSummary->setWordWrap(true);
templateDetailsLayout->addWidget(m_templateSummary);
QLabel* includedGemsTitle = new QLabel(tr("Included Gems"), this);
includedGemsTitle->setObjectName("includedGemsTitle");
templateDetailsLayout->addWidget(includedGemsTitle);
m_templateIncludedGems = new TagContainerWidget(this);
m_templateIncludedGems->setObjectName("includedGems");
templateDetailsLayout->addWidget(m_templateIncludedGems);
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
QLabel* moreGemsLabel = new QLabel(tr("Looking for more Gems?"), this);
moreGemsLabel->setObjectName("moreGems");
templateDetailsLayout->addWidget(moreGemsLabel);
QLabel* browseCatalogLabel = new QLabel(tr("Browse the Gems Catalog to further customize your project."), this);
browseCatalogLabel->setObjectName("browseCatalog");
browseCatalogLabel->setWordWrap(true);
templateDetailsLayout->addWidget(browseCatalogLabel);
QPushButton* configureGemsButton = new QPushButton(tr("Configure with more Gems"), this);
connect(configureGemsButton, &QPushButton::clicked, this, [=]()
{
emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog);
});
templateDetailsLayout->addWidget(configureGemsButton);
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
}
projectTemplateDetails->setLayout(templateDetailsLayout);
return projectTemplateDetails;
}
void NewProjectSettingsScreen::UpdateTemplateDetails(const ProjectTemplateInfo& templateInfo)
{
m_templateDisplayName->setText(templateInfo.m_displayName);
m_templateSummary->setText(templateInfo.m_summary);
m_templateIncludedGems->Update(templateInfo.m_includedGems);
}
} // namespace O3DE::ProjectManager
@@ -13,12 +13,17 @@
#if !defined(Q_MOC_RUN)
#include <ProjectSettingsScreen.h>
#include <ProjectTemplateInfo.h>
#include <QVector>
#endif
QT_FORWARD_DECLARE_CLASS(QButtonGroup)
QT_FORWARD_DECLARE_CLASS(QLabel)
QT_FORWARD_DECLARE_CLASS(QFrame)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(TagContainerWidget)
class NewProjectSettingsScreen
: public ProjectSettingsScreen
{
@@ -33,8 +38,17 @@ namespace O3DE::ProjectManager
private:
QString GetDefaultProjectPath();
QFrame* CreateTemplateDetails(int margin);
void UpdateTemplateDetails(const ProjectTemplateInfo& templateInfo);
QButtonGroup* m_projectTemplateButtonGroup;
QLabel* m_templateDisplayName;
QLabel* m_templateSummary;
TagContainerWidget* m_templateIncludedGems;
QVector<ProjectTemplateInfo> m_templates;
inline constexpr static int s_spacerSize = 20;
inline constexpr static int s_templateDetailsContentMargin = 20;
};
} // namespace O3DE::ProjectManager
@@ -0,0 +1,250 @@
/*
* 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 <ProjectBuilder.h>
#include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h>
#include <QProcess>
#include <QFile>
#include <QTextStream>
#include <QMessageBox>
#include <QDesktopServices>
#include <QUrl>
#include <QDir>
#include <QProcessEnvironment>
//#define MOCK_BUILD_PROJECT true
namespace O3DE::ProjectManager
{
// 10 Minutes
constexpr int MaxBuildTimeMSecs = 600000;
static const QString BuildPathPostfix = "windows_vs2019";
static const QString ErrorLogPathPostfix = "CMakeFiles/CMakeProjectBuildError.log";
ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo)
: QObject()
, m_projectInfo(projectInfo)
{
}
void ProjectBuilderWorker::BuildProject()
{
#ifdef MOCK_BUILD_PROJECT
for (int i = 0; i < 10; ++i)
{
QThread::sleep(1);
UpdateProgress(i * 10);
}
Done(m_projectPath);
#else
EngineInfo engineInfo;
AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
if (engineInfoResult.IsSuccess())
{
engineInfo = engineInfoResult.GetValue();
}
else
{
emit Done(tr("Failed to get engine info."));
return;
}
// Show some kind of progress with very approximate estimates
UpdateProgress(1);
QProcessEnvironment currentEnvironment(QProcessEnvironment::systemEnvironment());
// Append cmake path to PATH incase it is missing
QDir cmakePath(engineInfo.m_path);
cmakePath.cd("cmake/runtime/bin");
QString pathValue = currentEnvironment.value("PATH");
pathValue += ";" + cmakePath.path();
currentEnvironment.insert("PATH", pathValue);
QProcess configProjectProcess;
configProjectProcess.setProcessChannelMode(QProcess::MergedChannels);
configProjectProcess.setWorkingDirectory(m_projectInfo.m_path);
configProjectProcess.setProcessEnvironment(currentEnvironment);
configProjectProcess.start(
"cmake",
QStringList
{
"-B",
QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
"-S",
m_projectInfo.m_path,
"-G",
"Visual Studio 16",
"-DLY_3RDPARTY_PATH=" + engineInfo.m_thirdPartyPath
});
if (!configProjectProcess.waitForStarted())
{
emit Done(tr("Configuring project failed to start."));
return;
}
if (!configProjectProcess.waitForFinished(MaxBuildTimeMSecs))
{
WriteErrorLog(configProjectProcess.readAllStandardOutput());
emit Done(tr("Configuring project timed out. See log for details"));
return;
}
QString configProjectOutput(configProjectProcess.readAllStandardOutput());
if (configProjectProcess.exitCode() != 0 || !configProjectOutput.contains("Generating done"))
{
WriteErrorLog(configProjectOutput);
emit Done(tr("Configuring project failed. See log for details."));
return;
}
UpdateProgress(20);
QProcess buildProjectProcess;
buildProjectProcess.setProcessChannelMode(QProcess::MergedChannels);
buildProjectProcess.setWorkingDirectory(m_projectInfo.m_path);
buildProjectProcess.setProcessEnvironment(currentEnvironment);
buildProjectProcess.start(
"cmake",
QStringList
{
"--build",
QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
"--target",
m_projectInfo.m_projectName + ".GameLauncher",
"Editor",
"--config",
"profile"
});
if (!buildProjectProcess.waitForStarted())
{
emit Done(tr("Building project failed to start."));
return;
}
if (!buildProjectProcess.waitForFinished(MaxBuildTimeMSecs))
{
WriteErrorLog(configProjectProcess.readAllStandardOutput());
emit Done(tr("Building project timed out. See log for details"));
return;
}
QString buildProjectOutput(buildProjectProcess.readAllStandardOutput());
if (configProjectProcess.exitCode() != 0)
{
WriteErrorLog(buildProjectOutput);
emit Done(tr("Building project failed. See log for details."));
}
else
{
emit Done("");
}
#endif
}
QString ProjectBuilderWorker::LogFilePath() const
{
QDir logFilePath(m_projectInfo.m_path);
logFilePath.cd(BuildPathPostfix);
return logFilePath.filePath(ErrorLogPathPostfix);
}
void ProjectBuilderWorker::WriteErrorLog(const QString& log)
{
QFile logFile(LogFilePath());
// Overwrite file with truncate
if (logFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
QTextStream output(&logFile);
output << log;
logFile.close();
}
}
ProjectBuilderController::ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent)
: QObject()
, m_projectInfo(projectInfo)
, m_projectButton(projectButton)
, m_parent(parent)
{
m_worker = new ProjectBuilderWorker(m_projectInfo);
m_worker->moveToThread(&m_workerThread);
connect(&m_workerThread, &QThread::finished, m_worker, &ProjectBuilderWorker::deleteLater);
connect(&m_workerThread, &QThread::started, m_worker, &ProjectBuilderWorker::BuildProject);
connect(m_worker, &ProjectBuilderWorker::Done, this, &ProjectBuilderController::HandleResults);
connect(m_worker, &ProjectBuilderWorker::UpdateProgress, this, &ProjectBuilderController::UpdateUIProgress);
}
ProjectBuilderController::~ProjectBuilderController()
{
m_workerThread.quit();
m_workerThread.wait();
}
void ProjectBuilderController::Start()
{
m_workerThread.start();
UpdateUIProgress(0);
}
void ProjectBuilderController::SetProjectButton(ProjectButton* projectButton)
{
m_projectButton = projectButton;
}
QString ProjectBuilderController::GetProjectPath() const
{
return m_projectInfo.m_path;
}
void ProjectBuilderController::UpdateUIProgress(int progress)
{
if (m_projectButton)
{
m_projectButton->SetButtonOverlayText(QString("%1 (%2%)\n\n").arg(tr("Building Project..."), QString::number(progress)));
m_projectButton->SetProgressBarValue(progress);
}
}
void ProjectBuilderController::HandleResults(const QString& result)
{
if (!result.isEmpty())
{
if (result.contains(tr("log")))
{
QMessageBox::StandardButton openLog = QMessageBox::critical(
m_parent,
tr("Project Failed to Build!"),
result + tr("\n\nWould you like to view log?"),
QMessageBox::No | QMessageBox::Yes);
if (openLog == QMessageBox::Yes)
{
// Open application assigned to this file type
QDesktopServices::openUrl(QUrl("file:///" + m_worker->LogFilePath()));
}
}
else
{
QMessageBox::critical(m_parent, tr("Project Failed to Build!"), result);
}
}
emit Done();
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,73 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <ProjectInfo.h>
#include <QThread>
#endif
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(ProjectButton)
class ProjectBuilderWorker : public QObject
{
Q_OBJECT
public:
explicit ProjectBuilderWorker(const ProjectInfo& projectInfo);
~ProjectBuilderWorker() = default;
QString LogFilePath() const;
public slots:
void BuildProject();
signals:
void UpdateProgress(int progress);
void Done(QString result);
private:
void WriteErrorLog(const QString& log);
ProjectInfo m_projectInfo;
};
class ProjectBuilderController : public QObject
{
Q_OBJECT
public:
explicit ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent = nullptr);
~ProjectBuilderController();
void SetProjectButton(ProjectButton* projectButton);
QString GetProjectPath() const;
public slots:
void Start();
void UpdateUIProgress(int progress);
void HandleResults(const QString& result);
signals:
void Done();
private:
ProjectInfo m_projectInfo;
ProjectBuilderWorker* m_worker;
QThread m_workerThread;
ProjectButton* m_projectButton;
QWidget* m_parent;
};
} // namespace O3DE::ProjectManager
@@ -11,7 +11,7 @@
*/
#include <ProjectButtonWidget.h>
#include <AzQtComponents/Utilities/DesktopUtilities.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -21,6 +21,7 @@
#include <QPixmap>
#include <QMenu>
#include <QSpacerItem>
#include <QProgressBar>
namespace O3DE::ProjectManager
{
@@ -31,11 +32,26 @@ namespace O3DE::ProjectManager
: QLabel(parent)
{
setObjectName("labelButton");
QVBoxLayout* vLayout = new QVBoxLayout(this);
vLayout->setContentsMargins(0, 0, 0, 0);
vLayout->setSpacing(5);
setLayout(vLayout);
m_overlayLabel = new QLabel("", this);
m_overlayLabel->setObjectName("labelButtonOverlay");
m_overlayLabel->setWordWrap(true);
m_overlayLabel->setAlignment(Qt::AlignCenter);
m_overlayLabel->setVisible(false);
vLayout->addWidget(m_overlayLabel);
m_buildButton = new QPushButton(tr("Build Project"), this);
m_buildButton->setVisible(false);
m_progressBar = new QProgressBar(this);
m_progressBar->setObjectName("labelButtonProgressBar");
m_progressBar->setVisible(false);
vLayout->addWidget(m_progressBar);
}
void LabelButton::mousePressEvent([[maybe_unused]] QMouseEvent* event)
@@ -57,7 +73,22 @@ namespace O3DE::ProjectManager
m_overlayLabel->setText(text);
}
ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent)
QLabel* LabelButton::GetOverlayLabel()
{
return m_overlayLabel;
}
QProgressBar* LabelButton::GetProgressBar()
{
return m_progressBar;
}
QPushButton* LabelButton::GetBuildButton()
{
return m_buildButton;
}
ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent, bool processing)
: QFrame(parent)
, m_projectInfo(projectInfo)
{
@@ -66,10 +97,18 @@ namespace O3DE::ProjectManager
m_projectInfo.m_imagePath = ":/DefaultProjectImage.png";
}
Setup();
BaseSetup();
if (processing)
{
ProcessingSetup();
}
else
{
ReadySetup();
}
}
void ProjectButton::Setup()
void ProjectButton::BaseSetup()
{
setObjectName("projectButton");
@@ -81,49 +120,81 @@ namespace O3DE::ProjectManager
m_projectImageLabel = new LabelButton(this);
m_projectImageLabel->setFixedSize(s_projectImageWidth, s_projectImageHeight);
m_projectImageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
vLayout->addWidget(m_projectImageLabel);
m_projectImageLabel->setPixmap(
QPixmap(m_projectInfo.m_imagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding));
QMenu* newProjectMenu = new QMenu(this);
m_editProjectAction = newProjectMenu->addAction(tr("Edit Project Settings..."));
newProjectMenu->addSeparator();
m_copyProjectAction = newProjectMenu->addAction(tr("Duplicate"));
newProjectMenu->addSeparator();
m_removeProjectAction = newProjectMenu->addAction(tr("Remove from O3DE"));
m_deleteProjectAction = newProjectMenu->addAction(tr("Delete this Project"));
QFrame* footer = new QFrame(this);
m_projectFooter = new QFrame(this);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setContentsMargins(0, 0, 0, 0);
footer->setLayout(hLayout);
m_projectFooter->setLayout(hLayout);
{
QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this);
hLayout->addWidget(projectNameLabel);
QPushButton* projectMenuButton = new QPushButton(this);
projectMenuButton->setObjectName("projectMenuButton");
projectMenuButton->setMenu(newProjectMenu);
hLayout->addWidget(projectMenuButton);
}
vLayout->addWidget(footer);
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
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); });
vLayout->addWidget(m_projectFooter);
}
void ProjectButton::SetButtonEnabled(bool enabled)
void ProjectButton::ProcessingSetup()
{
m_projectImageLabel->GetOverlayLabel()->setAlignment(Qt::AlignHCenter | Qt::AlignBottom);
m_projectImageLabel->SetEnabled(false);
m_projectImageLabel->SetOverlayText(tr("Processing...\n\n"));
QProgressBar* progressBar = m_projectImageLabel->GetProgressBar();
progressBar->setVisible(true);
progressBar->setValue(0);
}
void ProjectButton::ReadySetup()
{
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
connect(m_projectImageLabel->GetBuildButton(), &QPushButton::clicked, [this](){ emit BuildProject(m_projectInfo); });
QMenu* menu = new QMenu(this);
menu->addAction(tr("Edit Project Settings..."), this, [this]() { emit EditProject(m_projectInfo.m_path); });
menu->addAction(tr("Build"), this, [this]() { emit BuildProject(m_projectInfo); });
menu->addSeparator();
menu->addAction(tr("Open Project folder..."), this, [this]()
{
AzQtComponents::ShowFileOnDesktop(m_projectInfo.m_path);
});
menu->addSeparator();
menu->addAction(tr("Duplicate"), this, [this]() { emit CopyProject(m_projectInfo.m_path); });
menu->addSeparator();
menu->addAction(tr("Remove from O3DE"), this, [this]() { emit RemoveProject(m_projectInfo.m_path); });
menu->addAction(tr("Delete this Project"), this, [this]() { emit DeleteProject(m_projectInfo.m_path); });
QPushButton* projectMenuButton = new QPushButton(this);
projectMenuButton->setObjectName("projectMenuButton");
projectMenuButton->setMenu(menu);
m_projectFooter->layout()->addWidget(projectMenuButton);
}
void ProjectButton::SetLaunchButtonEnabled(bool enabled)
{
m_projectImageLabel->SetEnabled(enabled);
}
void ProjectButton::ShowBuildButton(bool show)
{
QSpacerItem* buttonSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding);
m_projectImageLabel->layout()->addItem(buttonSpacer);
m_projectImageLabel->layout()->addWidget(m_projectImageLabel->GetBuildButton());
m_projectImageLabel->GetBuildButton()->setVisible(show);
}
void ProjectButton::SetButtonOverlayText(const QString& text)
{
m_projectImageLabel->SetOverlayText(text);
}
void ProjectButton::SetProgressBarValue(int progress)
{
m_projectImageLabel->GetProgressBar()->setValue(progress);
}
} // namespace O3DE::ProjectManager
@@ -21,6 +21,7 @@
QT_FORWARD_DECLARE_CLASS(QPixmap)
QT_FORWARD_DECLARE_CLASS(QPushButton)
QT_FORWARD_DECLARE_CLASS(QAction)
QT_FORWARD_DECLARE_CLASS(QProgressBar)
namespace O3DE::ProjectManager
{
@@ -36,6 +37,10 @@ namespace O3DE::ProjectManager
void SetEnabled(bool enabled);
void SetOverlayText(const QString& text);
QLabel* GetOverlayLabel();
QProgressBar* GetProgressBar();
QPushButton* GetBuildButton();
signals:
void triggered();
@@ -44,6 +49,8 @@ namespace O3DE::ProjectManager
private:
QLabel* m_overlayLabel;
QProgressBar* m_progressBar;
QPushButton* m_buildButton;
bool m_enabled = true;
};
@@ -53,11 +60,13 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr);
explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr, bool processing = false);
~ProjectButton() = default;
void SetButtonEnabled(bool enabled);
void SetLaunchButtonEnabled(bool enabled);
void ShowBuildButton(bool show);
void SetButtonOverlayText(const QString& text);
void SetProgressBarValue(int progress);
signals:
void OpenProject(const QString& projectName);
@@ -65,15 +74,15 @@ namespace O3DE::ProjectManager
void CopyProject(const QString& projectName);
void RemoveProject(const QString& projectName);
void DeleteProject(const QString& projectName);
void BuildProject(const ProjectInfo& projectInfo);
private:
void Setup();
void BaseSetup();
void ProcessingSetup();
void ReadySetup();
ProjectInfo m_projectInfo;
LabelButton* m_projectImageLabel;
QAction* m_editProjectAction;
QAction* m_copyProjectAction;
QAction* m_removeProjectAction;
QAction* m_deleteProjectAction;
QFrame* m_projectFooter;
};
} // namespace O3DE::ProjectManager
@@ -15,13 +15,16 @@
namespace O3DE::ProjectManager
{
ProjectInfo::ProjectInfo(const QString& path, const QString& projectName, const QString& displayName,
const QString& imagePath, const QString& backgroundImagePath, bool isNew)
const QString& origin, const QString& summary, const QString& imagePath, const QString& backgroundImagePath,
bool needsBuild)
: m_path(path)
, m_projectName(projectName)
, m_displayName(displayName)
, m_origin(origin)
, m_summary(summary)
, m_imagePath(imagePath)
, m_backgroundImagePath(backgroundImagePath)
, m_isNew(isNew)
, m_needsBuild(needsBuild)
{
}
+17 -3
View File
@@ -15,6 +15,7 @@
#if !defined(Q_MOC_RUN)
#include <AzCore/Math/Uuid.h>
#include <QString>
#include <QStringList>
#endif
namespace O3DE::ProjectManager
@@ -23,8 +24,17 @@ namespace O3DE::ProjectManager
{
public:
ProjectInfo() = default;
ProjectInfo(const QString& path, const QString& projectName, const QString& displayName,
const QString& imagePath, const QString& backgroundImagePath, bool isNew);
ProjectInfo(
const QString& path,
const QString& projectName,
const QString& displayName,
const QString& origin,
const QString& summary,
const QString& imagePath,
const QString& backgroundImagePath,
bool needsBuild);
bool operator==(const ProjectInfo& rhs);
bool operator!=(const ProjectInfo& rhs);
@@ -36,12 +46,16 @@ namespace O3DE::ProjectManager
// From project.json
QString m_projectName;
QString m_displayName;
QString m_origin;
QString m_summary;
QStringList m_userTags;
// Used on projects home screen
QString m_imagePath;
QString m_backgroundImagePath;
// Used in project creation
bool m_isNew = false; //! Is this a new project or existing
bool m_needsBuild = false; //! Does this project need to be built
};
} // namespace O3DE::ProjectManager
@@ -31,6 +31,7 @@ namespace O3DE::ProjectManager
QString m_name;
QString m_path;
QString m_summary;
QStringList m_includedGems;
QStringList m_canonicalTags;
QStringList m_userTags;
};
@@ -16,7 +16,9 @@
#include <QFileDialog>
#include <QDir>
#include <QMessageBox>
#include <QProgressDialog>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
namespace O3DE::ProjectManager
{
@@ -192,6 +194,49 @@ namespace O3DE::ProjectManager
return true;
}
static bool IsVS2019Installed_internal()
{
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
QString programFilesPath = environment.value("ProgramFiles(x86)");
QString vsWherePath = programFilesPath + "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
QFileInfo vsWhereFile(vsWherePath);
if (vsWhereFile.exists() && vsWhereFile.isFile())
{
QProcess vsWhereProcess;
vsWhereProcess.setProcessChannelMode(QProcess::MergedChannels);
vsWhereProcess.start(
vsWherePath,
QStringList{ "-version", "16.0", "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property", "isComplete" });
if (!vsWhereProcess.waitForStarted())
{
return false;
}
while (vsWhereProcess.waitForReadyRead())
{
}
QString vsWhereOutput(vsWhereProcess.readAllStandardOutput());
if (vsWhereOutput.startsWith("1"))
{
return true;
}
}
return false;
}
bool IsVS2019Installed()
{
static bool vs2019Installed = IsVS2019Installed_internal();
return vs2019Installed;
}
ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
{
auto iter = s_ProjectManagerStringNames.find(screen);
@@ -202,6 +247,5 @@ namespace O3DE::ProjectManager
return ProjectManagerScreen::Invalid;
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager
@@ -25,6 +25,9 @@ namespace O3DE::ProjectManager
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);
bool IsVS2019Installed();
ProjectManagerScreen GetProjectManagerScreen(const QString& screen);
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager
@@ -15,6 +15,8 @@
#include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h>
#include <ProjectUtils.h>
#include <ProjectBuilder.h>
#include <ScreensCtrl.h>
#include <AzQtComponents/Components/FlowLayout.h>
#include <AzCore/Platform.h>
@@ -42,6 +44,8 @@
#include <QSettings>
#include <QMessageBox>
#include <QTimer>
#include <QQueue>
#include <QDir>
//#define DISPLAY_PROJECT_DEV_DATA true
@@ -66,6 +70,14 @@ namespace O3DE::ProjectManager
m_stack->addWidget(m_projectsContent);
vLayout->addWidget(m_stack);
connect(reinterpret_cast<ScreensCtrl*>(parent), &ScreensCtrl::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
}
ProjectsScreen::~ProjectsScreen()
{
delete m_currentBuilder;
}
QFrame* ProjectsScreen::CreateFirstTimeContent()
@@ -110,7 +122,7 @@ namespace O3DE::ProjectManager
return frame;
}
QFrame* ProjectsScreen::CreateProjectsContent()
QFrame* ProjectsScreen::CreateProjectsContent(QString buildProjectPath, ProjectButton** projectButton)
{
QFrame* frame = new QFrame(this);
frame->setObjectName("projectsContent");
@@ -158,30 +170,43 @@ namespace O3DE::ProjectManager
projectsScrollArea->setWidgetResizable(true);
#ifndef DISPLAY_PROJECT_DEV_DATA
// Iterate once to insert building project first
if (!buildProjectPath.isEmpty())
{
buildProjectPath = QDir::fromNativeSeparators(buildProjectPath);
for (auto project : projectsResult.GetValue())
{
if (QDir::fromNativeSeparators(project.m_path) == buildProjectPath)
{
ProjectButton* buildingProjectButton = CreateProjectButton(project, flowLayout, true);
if (projectButton)
{
*projectButton = buildingProjectButton;
}
break;
}
}
}
for (auto project : projectsResult.GetValue())
#else
ProjectInfo project = projectsResult.GetValue().at(0);
for (int i = 0; i < 15; i++)
#endif
{
ProjectButton* projectButton;
QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
QFileInfo doesPreviewExist(projectPreviewPath);
if (doesPreviewExist.exists() && doesPreviewExist.isFile())
// Add all other projects skipping building project
// Safe if no building project because it is just an empty string
if (project.m_path != buildProjectPath)
{
project.m_imagePath = projectPreviewPath;
ProjectButton* projectButtonWidget = CreateProjectButton(project, flowLayout);
if (RequiresBuildProjectIterator(project.m_path) != m_requiresBuild.end())
{
projectButtonWidget->ShowBuildButton(true);
}
}
projectButton = new ProjectButton(project, this);
flowLayout->addWidget(projectButton);
connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
}
layout->addWidget(projectsScrollArea);
@@ -191,6 +216,60 @@ namespace O3DE::ProjectManager
return frame;
}
ProjectButton* ProjectsScreen::CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing)
{
ProjectButton* projectButton;
QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
QFileInfo doesPreviewExist(projectPreviewPath);
if (doesPreviewExist.exists() && doesPreviewExist.isFile())
{
project.m_imagePath = projectPreviewPath;
}
projectButton = new ProjectButton(project, this, processing);
flowLayout->addWidget(projectButton);
if (!processing)
{
connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
}
connect(projectButton, &ProjectButton::BuildProject, this, &ProjectsScreen::QueueBuildProject);
return projectButton;
}
void ProjectsScreen::ResetProjectsContent()
{
// refresh the projects content by re-creating it for now
if (m_projectsContent)
{
m_stack->removeWidget(m_projectsContent);
m_projectsContent->deleteLater();
}
// Make sure to update builder with latest Project Button
if (m_currentBuilder)
{
ProjectButton* projectButtonPtr;
m_projectsContent = CreateProjectsContent(m_currentBuilder->GetProjectPath(), &projectButtonPtr);
m_currentBuilder->SetProjectButton(projectButtonPtr);
}
else
{
m_projectsContent = CreateProjectsContent();
}
m_stack->addWidget(m_projectsContent);
m_stack->setCurrentWidget(m_projectsContent);
}
ProjectManagerScreen ProjectsScreen::GetScreenEnum()
{
return ProjectManagerScreen::Projects;
@@ -237,7 +316,7 @@ namespace O3DE::ProjectManager
{
if (ProjectUtils::AddProjectDialog(this))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
ResetProjectsContent();
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
}
@@ -245,38 +324,47 @@ namespace O3DE::ProjectManager
{
if (!projectPath.isEmpty())
{
AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
AZStd::string executableFilename = "Editor";
AZ::IO::FixedMaxPath editorExecutablePath = executableDirectory / (executableFilename + AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
auto cmdPath = AZ::IO::FixedMaxPathString::format("%s -regset=\"/Amazon/AzCore/Bootstrap/project_path=%s\"", editorExecutablePath.c_str(), projectPath.toStdString().c_str());
if (!WarnIfInBuildQueue(projectPath))
{
AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
AZStd::string executableFilename = "Editor";
AZ::IO::FixedMaxPath editorExecutablePath = executableDirectory / (executableFilename + AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
auto cmdPath = AZ::IO::FixedMaxPathString::format(
"%s -regset=\"/Amazon/AzCore/Bootstrap/project_path=%s\"", editorExecutablePath.c_str(),
projectPath.toStdString().c_str());
AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
processLaunchInfo.m_commandlineParameters = cmdPath;
bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
if (!launchSucceeded)
{
AZ_Error("ProjectManager", false, "Failed to launch editor");
QMessageBox::critical( this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
}
else
{
// prevent the user from accidentally pressing the button while the editor is launching
// and let them know what's happening
ProjectButton* button = qobject_cast<ProjectButton*>(sender());
if (button)
AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
processLaunchInfo.m_commandlineParameters = cmdPath;
bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
if (!launchSucceeded)
{
button->SetButtonEnabled(false);
button->SetButtonOverlayText(tr("Opening Editor..."));
AZ_Error("ProjectManager", false, "Failed to launch editor");
QMessageBox::critical(
this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
}
else
{
// prevent the user from accidentally pressing the button while the editor is launching
// and let them know what's happening
ProjectButton* button = qobject_cast<ProjectButton*>(sender());
if (button)
{
button->SetLaunchButtonEnabled(false);
button->SetButtonOverlayText(tr("Opening Editor..."));
}
// enable the button after 3 seconds
constexpr int waitTimeInMs = 3000;
QTimer::singleShot(waitTimeInMs, this, [this, button] {
if (button)
// enable the button after 3 seconds
constexpr int waitTimeInMs = 3000;
QTimer::singleShot(
waitTimeInMs, this,
[this, button]
{
button->SetButtonEnabled(true);
}
});
if (button)
{
button->SetLaunchButtonEnabled(true);
}
});
}
}
}
else
@@ -288,38 +376,90 @@ namespace O3DE::ProjectManager
}
void ProjectsScreen::HandleEditProject(const QString& projectPath)
{
emit NotifyCurrentProject(projectPath);
emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
if (!WarnIfInBuildQueue(projectPath))
{
emit NotifyCurrentProject(projectPath);
emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
}
}
void ProjectsScreen::HandleCopyProject(const QString& projectPath)
{
// Open file dialog and choose location for copied project then register copy with O3DE
if (ProjectUtils::CopyProjectDialog(projectPath, this))
if (!WarnIfInBuildQueue(projectPath))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
// Open file dialog and choose location for copied project then register copy with O3DE
if (ProjectUtils::CopyProjectDialog(projectPath, this))
{
ResetProjectsContent();
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
}
}
void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
{
// Unregister Project from O3DE and reload projects
if (ProjectUtils::UnregisterProject(projectPath))
if (!WarnIfInBuildQueue(projectPath))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
// Unregister Project from O3DE and reload projects
if (ProjectUtils::UnregisterProject(projectPath))
{
ResetProjectsContent();
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
}
}
void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(
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)
if (!WarnIfInBuildQueue(projectPath))
{
// Remove project from O3DE and delete from disk
HandleRemoveProject(projectPath);
ProjectUtils::DeleteProjectFiles(projectPath);
QMessageBox::StandardButton warningResult = QMessageBox::warning(this,
tr("Delete Project"),
tr("Are you sure?\nProject will be unregistered from O3DE and project directory will be deleted from your disk."),
QMessageBox::No | QMessageBox::Yes);
if (warningResult == QMessageBox::Yes)
{
// Remove project from O3DE and delete from disk
HandleRemoveProject(projectPath);
ProjectUtils::DeleteProjectFiles(projectPath);
}
}
}
void ProjectsScreen::SuggestBuildProject(const ProjectInfo& projectInfo)
{
if (projectInfo.m_needsBuild)
{
if (RequiresBuildProjectIterator(projectInfo.m_path) == m_requiresBuild.end())
{
m_requiresBuild.append(projectInfo);
}
ResetProjectsContent();
}
else
{
QMessageBox::information(this,
tr("Project Should be rebuilt."),
projectInfo.m_projectName + tr(" project likely needs to be rebuilt."));
}
}
void ProjectsScreen::QueueBuildProject(const ProjectInfo& projectInfo)
{
auto requiredIter = RequiresBuildProjectIterator(projectInfo.m_path);
if (requiredIter != m_requiresBuild.end())
{
m_requiresBuild.erase(requiredIter);
}
if (!BuildQueueContainsProject(projectInfo.m_path))
{
if (m_buildQueue.empty() && !m_currentBuilder)
{
StartProjectBuild(projectInfo);
}
else
{
m_buildQueue.append(projectInfo);
}
}
}
@@ -331,17 +471,7 @@ 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);
ResetProjectsContent();
}
}
@@ -363,4 +493,89 @@ namespace O3DE::ProjectManager
return displayFirstTimeContent;
}
void ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo)
{
if (ProjectUtils::IsVS2019Installed())
{
QMessageBox::StandardButton buildProject = QMessageBox::information(
this,
tr("Building \"%1\"").arg(projectInfo.m_projectName),
tr("Ready to build \"%1\"?").arg(projectInfo.m_projectName),
QMessageBox::No | QMessageBox::Yes);
if (buildProject == QMessageBox::Yes)
{
m_currentBuilder = new ProjectBuilderController(projectInfo, nullptr, this);
ResetProjectsContent();
connect(m_currentBuilder, &ProjectBuilderController::Done, this, &ProjectsScreen::ProjectBuildDone);
m_currentBuilder->Start();
}
else
{
ProjectBuildDone();
}
}
}
void ProjectsScreen::ProjectBuildDone()
{
delete m_currentBuilder;
m_currentBuilder = nullptr;
if (!m_buildQueue.empty())
{
StartProjectBuild(m_buildQueue.front());
m_buildQueue.pop_front();
}
else
{
ResetProjectsContent();
}
}
QList<ProjectInfo>::iterator ProjectsScreen::RequiresBuildProjectIterator(const QString& projectPath)
{
QString nativeProjPath(QDir::toNativeSeparators(projectPath));
auto projectIter = m_requiresBuild.begin();
for (; projectIter != m_requiresBuild.end(); ++projectIter)
{
if (QDir::toNativeSeparators(projectIter->m_path) == nativeProjPath)
{
break;
}
}
return projectIter;
}
bool ProjectsScreen::BuildQueueContainsProject(const QString& projectPath)
{
QString nativeProjPath(QDir::toNativeSeparators(projectPath));
for (const ProjectInfo& project : m_buildQueue)
{
if (QDir::toNativeSeparators(project.m_path) == nativeProjPath)
{
return true;
}
}
return false;
}
bool ProjectsScreen::WarnIfInBuildQueue(const QString& projectPath)
{
if (BuildQueueContainsProject(projectPath))
{
QMessageBox::warning(
this,
tr("Action Temporarily Disabled!"),
tr("Action not allowed on projects in build queue."));
return true;
}
return false;
}
} // namespace O3DE::ProjectManager
@@ -13,21 +13,28 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <ProjectInfo.h>
#include <QQueue>
#endif
QT_FORWARD_DECLARE_CLASS(QPaintEvent)
QT_FORWARD_DECLARE_CLASS(QFrame)
QT_FORWARD_DECLARE_CLASS(QStackedWidget)
QT_FORWARD_DECLARE_CLASS(QLayout)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(ProjectBuilderController);
QT_FORWARD_DECLARE_CLASS(ProjectButton);
class ProjectsScreen
: public ScreenWidget
{
public:
explicit ProjectsScreen(QWidget* parent = nullptr);
~ProjectsScreen() = default;
~ProjectsScreen();
ProjectManagerScreen GetScreenEnum() override;
QString GetTabText() override;
@@ -35,6 +42,7 @@ namespace O3DE::ProjectManager
protected:
void NotifyCurrentScreen() override;
void ProjectBuildDone();
protected slots:
void HandleNewProjectButton();
@@ -45,19 +53,32 @@ namespace O3DE::ProjectManager
void HandleRemoveProject(const QString& projectPath);
void HandleDeleteProject(const QString& projectPath);
void SuggestBuildProject(const ProjectInfo& projectInfo);
void QueueBuildProject(const ProjectInfo& projectInfo);
void paintEvent(QPaintEvent* event) override;
private:
QFrame* CreateFirstTimeContent();
QFrame* CreateProjectsContent();
QFrame* CreateProjectsContent(QString buildProjectPath = "", ProjectButton** projectButton = nullptr);
ProjectButton* CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing = false);
void ResetProjectsContent();
bool ShouldDisplayFirstTimeContent();
QAction* m_createNewProjectAction;
QAction* m_addExistingProjectAction;
void StartProjectBuild(const ProjectInfo& projectInfo);
QList<ProjectInfo>::iterator RequiresBuildProjectIterator(const QString& projectPath);
bool BuildQueueContainsProject(const QString& projectPath);
bool WarnIfInBuildQueue(const QString& projectPath);
QAction* m_createNewProjectAction = nullptr;
QAction* m_addExistingProjectAction = nullptr;
QPixmap m_background;
QFrame* m_firstTimeContent;
QFrame* m_projectsContent;
QStackedWidget* m_stack;
QFrame* m_firstTimeContent = nullptr;
QFrame* m_projectsContent = nullptr;
QStackedWidget* m_stack = nullptr;
QList<ProjectInfo> m_requiresBuild;
QQueue<ProjectInfo> m_buildQueue;
ProjectBuilderController* m_currentBuilder = nullptr;
const QString m_projectPreviewImagePath = "/preview.png";
@@ -19,6 +19,7 @@
#include <pybind11/functional.h>
#include <pybind11/embed.h>
#include <pybind11/eval.h>
#include <pybind11/stl.h>
#pragma pop_macro("slots")
#include <AzCore/IO/FileIO.h>
@@ -289,12 +290,14 @@ namespace O3DE::ProjectManager
m_engineTemplate = pybind11::module::import("o3de.engine_template");
m_enableGemProject = pybind11::module::import("o3de.enable_gem");
m_disableGemProject = pybind11::module::import("o3de.disable_gem");
m_editProjectProperties = pybind11::module::import("o3de.project_properties");
// make sure the engine is registered
RegisterThisEngine();
return result == 0 && !PyErr_Occurred();
} catch ([[maybe_unused]] const std::exception& e)
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("ProjectManagerWindow", false, "Py_Initialize() failed with %s", e.what());
return false;
@@ -320,25 +323,25 @@ namespace O3DE::ProjectManager
bool registrationResult = true; // already registered is considered successful
bool pythonResult = ExecuteWithLock(
[&]
{
// check current engine path against all other registered engines
// to see if we are already registered
auto allEngines = m_manifest.attr("get_engines")();
if (pybind11::isinstance<pybind11::list>(allEngines))
{
// check current engine path against all other registered engines
// to see if we are already registered
auto allEngines = m_manifest.attr("get_engines")();
if (pybind11::isinstance<pybind11::list>(allEngines))
for (auto engine : allEngines)
{
for (auto engine : allEngines)
AZ::IO::FixedMaxPath enginePath(Py_To_String(engine["path"]));
if (enginePath.Compare(m_enginePath) == 0)
{
AZ::IO::FixedMaxPath enginePath(Py_To_String(engine["path"]));
if (enginePath.Compare(m_enginePath) == 0)
{
return;
}
return;
}
}
}
auto result = m_register.attr("register")(m_enginePath.c_str());
registrationResult = (result.cast<int>() == 0);
});
auto result = m_register.attr("register")(m_enginePath.c_str());
registrationResult = (result.cast<int>() == 0);
});
bool finalResult = (registrationResult && pythonResult);
AZ_Assert(finalResult, "Registration of this engine failed!");
@@ -378,12 +381,14 @@ namespace O3DE::ProjectManager
auto o3deData = m_manifest.attr("load_o3de_manifest")();
if (pybind11::isinstance<pybind11::dict>(o3deData))
{
engineInfo.m_path = Py_To_String(enginePath);
engineInfo.m_defaultGemsFolder = Py_To_String(o3deData["default_gems_folder"]);
engineInfo.m_defaultProjectsFolder = Py_To_String(o3deData["default_projects_folder"]);
engineInfo.m_path = Py_To_String(enginePath);
engineInfo.m_defaultGemsFolder = Py_To_String(o3deData["default_gems_folder"]);
engineInfo.m_defaultProjectsFolder = Py_To_String(o3deData["default_projects_folder"]);
engineInfo.m_defaultRestrictedFolder = Py_To_String(o3deData["default_restricted_folder"]);
engineInfo.m_defaultTemplatesFolder = Py_To_String(o3deData["default_templates_folder"]);
engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData,"third_party_path","");
engineInfo.m_defaultTemplatesFolder = Py_To_String(o3deData["default_templates_folder"]);
pybind11::str defaultThirdPartyFolder = m_manifest.attr("get_o3de_third_party_folder")();
engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData,"default_third_party_folder", Py_To_String(defaultThirdPartyFolder));
}
auto engineData = m_manifest.attr("get_engine_json_data")(pybind11::none(), enginePath);
@@ -391,8 +396,8 @@ namespace O3DE::ProjectManager
{
try
{
engineInfo.m_version = Py_To_String_Optional(engineData,"O3DEVersion","0.0.0.0");
engineInfo.m_name = Py_To_String_Optional(engineData,"engine_name","O3DE");
engineInfo.m_version = Py_To_String_Optional(engineData, "O3DEVersion", "0.0.0.0");
engineInfo.m_name = Py_To_String_Optional(engineData, "engine_name", "O3DE");
}
catch ([[maybe_unused]] const std::exception& e)
{
@@ -416,44 +421,32 @@ namespace O3DE::ProjectManager
bool PythonBindings::SetEngineInfo(const EngineInfo& engineInfo)
{
bool result = ExecuteWithLock([&] {
pybind11::str enginePath = engineInfo.m_path.toStdString();
pybind11::str defaultProjectsFolder = engineInfo.m_defaultProjectsFolder.toStdString();
pybind11::str defaultGemsFolder = engineInfo.m_defaultGemsFolder.toStdString();
pybind11::str enginePath = engineInfo.m_path.toStdString();
pybind11::str defaultProjectsFolder = engineInfo.m_defaultProjectsFolder.toStdString();
pybind11::str defaultGemsFolder = engineInfo.m_defaultGemsFolder.toStdString();
pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString();
pybind11::str defaultThirdPartyFolder = engineInfo.m_thirdPartyPath.toStdString();
auto registrationResult = m_register.attr("register")(
enginePath, // engine_path
pybind11::none(), // project_path
enginePath, // engine_path
pybind11::none(), // project_path
pybind11::none(), // gem_path
pybind11::none(), // external_subdir_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
pybind11::none(), // repo_uri
pybind11::none(), // external_subdir_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
pybind11::none(), // repo_uri
pybind11::none(), // default_engines_folder
defaultProjectsFolder,
defaultGemsFolder,
defaultTemplatesFolder
defaultTemplatesFolder,
pybind11::none(), // default_restricted_folder
defaultThirdPartyFolder
);
if (registrationResult.cast<int>() != 0)
{
result = false;
}
auto manifest = m_manifest.attr("load_o3de_manifest")();
if (pybind11::isinstance<pybind11::dict>(manifest))
{
try
{
manifest["third_party_path"] = engineInfo.m_thirdPartyPath.toStdString();
m_manifest.attr("save_o3de_manifest")(manifest);
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("PythonBindings", false, "Failed to set third party path.");
}
}
});
return result;
@@ -477,12 +470,12 @@ namespace O3DE::ProjectManager
QVector<GemInfo> gems;
auto result = ExecuteWithLockErrorHandling([&]
{
for (auto path : m_manifest.attr("get_engine_gems")())
{
for (auto path : m_manifest.attr("get_engine_gems")())
{
gems.push_back(GemInfoFromPath(path));
}
});
gems.push_back(GemInfoFromPath(path));
}
});
if (!result.IsSuccess())
{
return AZ::Failure<AZStd::string>(result.GetError().c_str());
@@ -497,13 +490,13 @@ namespace O3DE::ProjectManager
QVector<GemInfo> gems;
auto result = ExecuteWithLockErrorHandling([&]
{
pybind11::str pyProjectPath = projectPath.toStdString();
for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath))
{
pybind11::str pyProjectPath = projectPath.toStdString();
for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath))
{
gems.push_back(GemInfoFromPath(path));
}
});
gems.push_back(GemInfoFromPath(path));
}
});
if (!result.IsSuccess())
{
return AZ::Failure<AZStd::string>(result.GetError().c_str());
@@ -518,12 +511,12 @@ namespace O3DE::ProjectManager
// Retrieve the path to the cmake file that lists the enabled gems.
pybind11::str enabledGemsFilename;
auto result = ExecuteWithLockErrorHandling([&]
{
const pybind11::str pyProjectPath = projectPath.toStdString();
enabledGemsFilename = m_cmake.attr("get_enabled_gem_cmake_file")(
pybind11::none(), // project_name
pyProjectPath); // project_path
});
{
const pybind11::str pyProjectPath = projectPath.toStdString();
enabledGemsFilename = m_cmake.attr("get_enabled_gem_cmake_file")(
pybind11::none(), // project_name
pyProjectPath); // project_path
});
if (!result.IsSuccess())
{
return AZ::Failure<AZStd::string>(result.GetError().c_str());
@@ -532,13 +525,13 @@ namespace O3DE::ProjectManager
// Retrieve the actual list of names from the cmake file.
QVector<AZStd::string> gemNames;
result = ExecuteWithLockErrorHandling([&]
{
const auto pyGemNames = m_cmake.attr("get_enabled_gems")(enabledGemsFilename);
for (auto gemName : pyGemNames)
{
const auto pyGemNames = m_cmake.attr("get_enabled_gems")(enabledGemsFilename);
for (auto gemName : pyGemNames)
{
gemNames.push_back(Py_To_String(gemName));
}
});
gemNames.push_back(Py_To_String(gemName));
}
});
if (!result.IsSuccess())
{
return AZ::Failure<AZStd::string>(result.GetError().c_str());
@@ -552,13 +545,13 @@ namespace O3DE::ProjectManager
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
pybind11::str projectPath = path.toStdString();
auto pythonRegistrationResult = m_register.attr("register")(pybind11::none(), projectPath);
{
pybind11::str projectPath = path.toStdString();
auto pythonRegistrationResult = m_register.attr("register")(pybind11::none(), projectPath);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
return result && registrationResult;
}
@@ -568,30 +561,31 @@ namespace O3DE::ProjectManager
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
pybind11::str projectPath = path.toStdString();
auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), // engine_path
projectPath, // project_path
pybind11::none(), // gem_path
pybind11::none(), // external_subdir_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
pybind11::none(), // repo_uri
pybind11::none(), // default_engines_folder
pybind11::none(), // default_projects_folder
pybind11::none(), // default_gems_folder
pybind11::none(), // default_templates_folder
pybind11::none(), // default_restricted_folder
pybind11::none(), // external_subdir_engine_path
pybind11::none(), // external_subdir_project_path
true, // remove
false // force
{
pybind11::str projectPath = path.toStdString();
auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), // engine_path
projectPath, // project_path
pybind11::none(), // gem_path
pybind11::none(), // external_subdir_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
pybind11::none(), // repo_uri
pybind11::none(), // default_engines_folder
pybind11::none(), // default_projects_folder
pybind11::none(), // default_gems_folder
pybind11::none(), // default_templates_folder
pybind11::none(), // default_restricted_folder
pybind11::none(), // default_third_party_folder
pybind11::none(), // external_subdir_engine_path
pybind11::none(), // external_subdir_project_path
true, // remove
false // force
);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
return result && registrationResult;
}
@@ -649,12 +643,12 @@ namespace O3DE::ProjectManager
try
{
// required
gemInfo.m_name = Py_To_String(data["gem_name"]);
gemInfo.m_name = Py_To_String(data["gem_name"]);
// optional
gemInfo.m_displayName = Py_To_String_Optional(data, "DisplayName", gemInfo.m_name);
gemInfo.m_summary = Py_To_String_Optional(data, "Summary", "");
gemInfo.m_version = Py_To_String_Optional(data, "Version", "");
gemInfo.m_summary = Py_To_String_Optional(data, "Summary", "");
gemInfo.m_version = Py_To_String_Optional(data, "Version", "");
if (data.contains("Tags"))
{
@@ -677,7 +671,7 @@ namespace O3DE::ProjectManager
{
ProjectInfo projectInfo;
projectInfo.m_path = Py_To_String(path);
projectInfo.m_isNew = false;
projectInfo.m_needsBuild = false;
auto projectData = m_manifest.attr("get_project_json_data")(pybind11::none(), path);
if (pybind11::isinstance<pybind11::dict>(projectData))
@@ -685,7 +679,16 @@ namespace O3DE::ProjectManager
try
{
projectInfo.m_projectName = Py_To_String(projectData["project_name"]);
projectInfo.m_displayName = Py_To_String_Optional(projectData,"display_name", projectInfo.m_projectName);
projectInfo.m_displayName = Py_To_String_Optional(projectData, "display_name", projectInfo.m_projectName);
projectInfo.m_origin = Py_To_String_Optional(projectData, "origin", projectInfo.m_origin);
projectInfo.m_summary = Py_To_String_Optional(projectData, "summary", projectInfo.m_summary);
if (projectData.contains("user_tags"))
{
for (auto tag : projectData["user_tags"])
{
projectInfo.m_userTags.append(Py_To_String(tag));
}
}
}
catch ([[maybe_unused]] const std::exception& e)
{
@@ -727,44 +730,62 @@ namespace O3DE::ProjectManager
AZ::Outcome<void, AZStd::string> PythonBindings::AddGemToProject(const QString& gemPath, const QString& projectPath)
{
return ExecuteWithLockErrorHandling([&]
{
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
{
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_enableGemProject.attr("enable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided
pyGemPath,
pybind11::none(), // project name not needed as path is provided
pyProjectPath
m_enableGemProject.attr("enable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided
pyGemPath,
pybind11::none(), // project name not needed as path is provided
pyProjectPath
);
});
});
}
AZ::Outcome<void, AZStd::string> PythonBindings::RemoveGemFromProject(const QString& gemPath, const QString& projectPath)
{
return ExecuteWithLockErrorHandling([&]
{
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
{
pybind11::str pyGemPath = gemPath.toStdString();
pybind11::str pyProjectPath = projectPath.toStdString();
m_disableGemProject.attr("disable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided
pyGemPath,
pybind11::none(), // project name not needed as path is provided
pyProjectPath
m_disableGemProject.attr("disable_gem_in_project")(
pybind11::none(), // gem name not needed as path is provided
pyGemPath,
pybind11::none(), // project name not needed as path is provided
pyProjectPath
);
});
});
}
bool PythonBindings::UpdateProject([[maybe_unused]] const ProjectInfo& projectInfo)
AZ::Outcome<void, AZStd::string> PythonBindings::UpdateProject(const ProjectInfo& projectInfo)
{
return false;
return ExecuteWithLockErrorHandling([&]
{
std::list<std::string> newTags;
for (const auto& i : projectInfo.m_userTags)
{
newTags.push_back(i.toStdString());
}
m_editProjectProperties.attr("edit_project_props")(
pybind11::str(projectInfo.m_path.toStdString()), // proj_path
pybind11::none(), // proj_name not used
pybind11::str(projectInfo.m_origin.toStdString()), // new_origin
pybind11::str(projectInfo.m_displayName.toStdString()), // new_display
pybind11::str(projectInfo.m_summary.toStdString()), // new_summary
pybind11::str(projectInfo.m_imagePath.toStdString()), // new_icon
pybind11::none(), // add_tags not used
pybind11::none(), // remove_tags not used
pybind11::list(pybind11::cast(newTags))); // replace_tags
});
}
ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path)
{
ProjectTemplateInfo templateInfo;
templateInfo.m_path = Py_To_String(path);
templateInfo.m_path = Py_To_String(pybind11::str(path));
auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path);
if (pybind11::isinstance<pybind11::dict>(data))
@@ -773,8 +794,8 @@ namespace O3DE::ProjectManager
{
// required
templateInfo.m_displayName = Py_To_String(data["display_name"]);
templateInfo.m_name = Py_To_String(data["template_name"]);
templateInfo.m_summary = Py_To_String(data["summary"]);
templateInfo.m_name = Py_To_String(data["template_name"]);
templateInfo.m_summary = Py_To_String(data["summary"]);
// optional
if (data.contains("canonical_tags"))
@@ -791,6 +812,13 @@ namespace O3DE::ProjectManager
templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
}
}
if (data.contains("included_gems"))
{
for (auto gem : data["included_gems"])
{
templateInfo.m_includedGems.push_back(Py_To_String(gem));
}
}
}
catch ([[maybe_unused]] const std::exception& e)
{
@@ -806,7 +834,7 @@ namespace O3DE::ProjectManager
QVector<ProjectTemplateInfo> templates;
bool result = ExecuteWithLock([&] {
for (auto path : m_manifest.attr("get_project_templates")())
for (auto path : m_manifest.attr("get_templates_for_project_creation")())
{
templates.push_back(ProjectTemplateInfoFromPath(path));
}
@@ -50,7 +50,7 @@ namespace O3DE::ProjectManager
AZ::Outcome<QVector<ProjectInfo>> GetProjects() override;
bool AddProject(const QString& path) override;
bool RemoveProject(const QString& path) override;
bool UpdateProject(const ProjectInfo& projectInfo) override;
AZ::Outcome<void, AZStd::string> UpdateProject(const ProjectInfo& projectInfo) override;
AZ::Outcome<void, AZStd::string> AddGemToProject(const QString& gemPath, const QString& projectPath) override;
AZ::Outcome<void, AZStd::string> RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override;
@@ -78,5 +78,6 @@ namespace O3DE::ProjectManager
pybind11::handle m_manifest;
pybind11::handle m_enableGemProject;
pybind11::handle m_disableGemProject;
pybind11::handle m_editProjectProperties;
};
}
@@ -122,7 +122,7 @@ namespace O3DE::ProjectManager
* @param projectInfo the info to use to update the project
* @return true on success, false on failure
*/
virtual bool UpdateProject(const ProjectInfo& projectInfo) = 0;
virtual AZ::Outcome<void, AZStd::string> UpdateProject(const ProjectInfo& projectInfo) = 0;
/**
* Add a gem to a project
@@ -13,6 +13,7 @@
#if !defined(Q_MOC_RUN)
#include <ScreenDefs.h>
#include <ProjectInfo.h>
#include <QWidget>
#include <QStyleOption>
@@ -61,6 +62,7 @@ namespace O3DE::ProjectManager
void GotoPreviousScreenRequest();
void ResetScreenRequest(ProjectManagerScreen screen);
void NotifyCurrentProject(const QString& projectPath);
void NotifyBuildProject(const ProjectInfo& projectInfo);
};
@@ -177,6 +177,7 @@ namespace O3DE::ProjectManager
connect(newScreen, &ScreenWidget::GotoPreviousScreenRequest, this, &ScreensCtrl::GotoPreviousScreen);
connect(newScreen, &ScreenWidget::ResetScreenRequest, this, &ScreensCtrl::ResetScreen);
connect(newScreen, &ScreenWidget::NotifyCurrentProject, this, &ScreensCtrl::NotifyCurrentProject);
connect(newScreen, &ScreenWidget::NotifyBuildProject, this, &ScreensCtrl::NotifyBuildProject);
}
void ScreensCtrl::ResetAllScreens()
@@ -13,6 +13,7 @@
#if !defined(Q_MOC_RUN)
#include <ScreenDefs.h>
#include <ProjectInfo.h>
#include <QStackedWidget>
#include <QStack>
@@ -39,6 +40,7 @@ namespace O3DE::ProjectManager
signals:
void NotifyCurrentProject(const QString& projectPath);
void NotifyBuildProject(const ProjectInfo& projectInfo);
public slots:
bool ChangeToScreen(ProjectManagerScreen screen);
@@ -0,0 +1,65 @@
/*
* 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 <TemplateButtonWidget.h>
#include <QVBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QAbstractButton>
#include <QStyle>
#include <QVariant>
namespace O3DE::ProjectManager
{
TemplateButton::TemplateButton(const QString& imagePath, const QString& labelText, QWidget* parent)
: QPushButton(parent)
{
setAutoExclusive(true);
setObjectName("templateButton");
QVBoxLayout* vLayout = new QVBoxLayout();
vLayout->setSpacing(0);
vLayout->setContentsMargins(0, 0, 0, 0);
setLayout(vLayout);
QLabel* image = new QLabel(this);
image->setObjectName("templateImage");
image->setPixmap(
QPixmap(imagePath).scaled(QSize(s_templateImageWidth,s_templateImageHeight) , Qt::KeepAspectRatio, Qt::SmoothTransformation));
vLayout->addWidget(image);
QLabel* label = new QLabel(labelText, this);
label->setObjectName("templateLabel");
vLayout->addWidget(label);
connect(this, &QAbstractButton::toggled, this, &TemplateButton::onToggled);
}
void TemplateButton::onToggled()
{
setProperty("Checked", isChecked());
// we must unpolish/polish every child after changing a property
// or else they won't use the correct stylesheet selector
for (auto child : findChildren<QWidget*>())
{
child->style()->unpolish(child);
child->style()->polish(child);
}
style()->unpolish(this);
style()->polish(this);
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,37 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <QPushButton>
#endif
namespace O3DE::ProjectManager
{
class TemplateButton
: public QPushButton
{
Q_OBJECT // AUTOMOC
public:
explicit TemplateButton(const QString& imagePath, const QString& labelText, QWidget* parent = nullptr);
~TemplateButton() = default;
protected slots:
void onToggled();
private:
inline constexpr static int s_templateImageWidth = 92;
inline constexpr static int s_templateImageHeight = 122;
};
} // namespace O3DE::ProjectManager
@@ -58,7 +58,7 @@ namespace O3DE::ProjectManager
tabWidget->tabBar()->setObjectName("projectSettingsTabBar");
tabWidget->addTab(m_updateSettingsScreen, tr("General"));
QPushButton* gemsButton = new QPushButton(tr("Add More Gems"), this);
QPushButton* gemsButton = new QPushButton(tr("Configure Gems"), this);
topBarHLayout->addWidget(gemsButton);
tabWidget->setCornerWidget(gemsButton);
@@ -119,7 +119,9 @@ namespace O3DE::ProjectManager
void UpdateProjectCtrl::HandleNextButton()
{
if (m_stack->currentIndex() == ScreenOrder::Settings)
bool shouldRebuild = false;
if (m_stack->currentIndex() == ScreenOrder::Settings && m_updateSettingsScreen)
{
if (m_updateSettingsScreen)
{
@@ -134,10 +136,10 @@ namespace O3DE::ProjectManager
// Update project if settings changed
if (m_projectInfo != newProjectSettings)
{
bool result = PythonBindingsInterface::Get()->UpdateProject(newProjectSettings);
if (!result)
auto result = PythonBindingsInterface::Get()->UpdateProject(newProjectSettings);
if (!result.IsSuccess())
{
QMessageBox::critical(this, tr("Project update failed"), tr("Failed to update project."));
QMessageBox::critical(this, tr("Project update failed"), tr(result.GetError().c_str()));
return;
}
}
@@ -155,11 +157,17 @@ namespace O3DE::ProjectManager
m_projectInfo = newProjectSettings;
}
}
if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
{
// Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path);
shouldRebuild = true;
}
if (shouldRebuild)
{
emit NotifyBuildProject(m_projectInfo);
}
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
@@ -181,7 +189,7 @@ namespace O3DE::ProjectManager
{
if (m_stack->currentIndex() == ScreenOrder::Gems)
{
m_header->setSubTitle(QString(tr("Add More Gems to \"%1\"")).arg(m_projectInfo.m_projectName));
m_header->setSubTitle(QString(tr("Configure Gems for \"%1\"")).arg(m_projectInfo.m_projectName));
m_nextButton->setText(tr("Confirm"));
}
else
@@ -38,6 +38,8 @@ set(FILES
Source/ProjectInfo.cpp
Source/ProjectUtils.h
Source/ProjectUtils.cpp
Source/ProjectBuilder.h
Source/ProjectBuilder.cpp
Source/UpdateProjectSettingsScreen.h
Source/UpdateProjectSettingsScreen.cpp
Source/NewProjectSettingsScreen.h
@@ -60,6 +62,8 @@ set(FILES
Source/LinkWidget.cpp
Source/TagWidget.h
Source/TagWidget.cpp
Source/TemplateButtonWidget.h
Source/TemplateButtonWidget.cpp
Source/GemCatalog/GemCatalogHeaderWidget.h
Source/GemCatalog/GemCatalogHeaderWidget.cpp
Source/GemCatalog/GemCatalogScreen.h
@@ -17,6 +17,7 @@
#include <AzCore/Utils/Utils.h>
#include <AzToolsFramework/Thumbnails/ThumbnailerNullComponent.h>
#include <SliceConverterEditorEntityContextComponent.h>
namespace AZ
{
@@ -34,6 +35,9 @@ namespace AZ
Application::Application(int argc, char** argv)
: AzToolsFramework::ToolsApplication(&argc, &argv)
{
// We need a specialized variant of EditorEntityContextCompnent for the SliceConverter, so we register the descriptor here.
RegisterComponentDescriptor(AzToolsFramework::SliceConverterEditorEntityContextComponent::CreateDescriptor());
AZ::IO::FixedMaxPath projectPath = AZ::Utils::GetProjectPath();
if (projectPath.empty())
{
@@ -110,10 +114,21 @@ namespace AZ
AZ::ComponentTypeList Application::GetRequiredSystemComponents() const
{
// Use all of the default system components, but also add in the ThumbnailerNullComponent so that components requiring
// a ThumbnailService can still be started up.
// By default, we use all of the standard system components.
AZ::ComponentTypeList components = AzToolsFramework::ToolsApplication::GetRequiredSystemComponents();
// Also add in the ThumbnailerNullComponent so that components requiring a ThumbnailService can still be started up.
components.emplace_back(azrtti_typeid<AzToolsFramework::Thumbnailer::ThumbnailerNullComponent>());
// The Slice Converter requires a specialized variant of the EditorEntityContextComponent that exposes the ability
// to disable the behavior of activating entities on creation. During conversion, the creation flow will be triggered,
// but entity activation requires a significant amount of subsystem initialization that's unneeded for conversion.
// So, to get around this, we swap out EditorEntityContextComponent with SliceConverterEditorEntityContextComponent.
components.erase(
AZStd::remove(
components.begin(), components.end(), azrtti_typeid<AzToolsFramework::EditorEntityContextComponent>()),
components.end());
components.emplace_back(azrtti_typeid<AzToolsFramework::SliceConverterEditorEntityContextComponent>());
return components;
}
} // namespace SerializeContextTools
@@ -30,13 +30,16 @@
#include <AzToolsFramework/Prefab/PrefabDomUtils.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
#include <AzToolsFramework/Prefab/Instance/InstanceUpdateExecutorInterface.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <Application.h>
#include <SliceConverter.h>
#include <SliceConverterEditorEntityContextComponent.h>
#include <Utilities.h>
// SliceConverter reads in a slice file (saved in an ObjectStream format), instantiates it, creates a prefab out of the data,
// and saves the prefab in a JSON format. This can be used for one-time migrations of slices or slice-based levels to prefabs.
//
@@ -99,12 +102,26 @@ namespace AZ
bool result = true;
rapidjson::StringBuffer scratchBuffer;
// For slice conversion, disable the EditorEntityContextComponent logic that activates entities on creation.
// This prevents a lot of error messages and crashes during conversion due to lack of full environment and subsystem setup.
AzToolsFramework::SliceConverterEditorEntityContextComponent::DisableOnContextEntityLogic();
// Loop through the list of requested files and convert them.
AZStd::vector<AZStd::string> fileList = Utilities::ReadFileListFromCommandLine(application, "files");
for (AZStd::string& filePath : fileList)
{
bool convertResult = ConvertSliceFile(convertSettings.m_serializeContext, filePath, isDryRun);
result = result && convertResult;
// Clear out all registered prefab templates between each top-level file that gets processed.
auto prefabSystemComponentInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
for (auto templateId : m_createdTemplateIds)
{
// We don't just want to call RemoveAllTemplates() because the root template should remain between file conversions.
prefabSystemComponentInterface->RemoveTemplate(templateId);
}
m_aliasIdMapper.clear();
m_createdTemplateIds.clear();
}
DisconnectFromAssetProcessor();
@@ -114,6 +131,13 @@ namespace AZ
bool SliceConverter::ConvertSliceFile(
AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun)
{
/* To convert a slice file, we read the input file in via ObjectStream, then use the "class ready" callback to convert
* the data in memory to a Prefab.
* If the input file is a level file (.ly), we actually need to load the level slice file ("levelentities.editor_xml") from
* within the level file, which effectively is a zip file of the level slice file and a bunch of legacy level files that won't
* be converted, since the systems that would use them no longer exist.
*/
bool result = true;
bool packOpened = false;
@@ -144,7 +168,7 @@ namespace AZ
AZ_STRING_ARG(fileExtension.Native()));
}
auto callback = [&outputPath, isDryRun](void* classPtr, const Uuid& classId, SerializeContext* context)
auto callback = [this, &outputPath, isDryRun](void* classPtr, const Uuid& classId, SerializeContext* context)
{
if (classId != azrtti_typeid<AZ::Entity>())
{
@@ -178,6 +202,13 @@ namespace AZ
bool SliceConverter::ConvertSliceToPrefab(
AZ::SerializeContext* serializeContext, AZ::IO::PathView outputPath, bool isDryRun, AZ::Entity* rootEntity)
{
/* Given a root slice entity, we convert it to a prefab by doing the following:
* - Locate the SliceComponent
* - Take all the entities directly located on the slice, and put them into a prefab
* - Fix up any top-level entities to have the prefab container entity as their parent
* - If there are any nested slice instances, convert the nested slices to prefabs, then convert the instances.
*/
auto prefabSystemComponentInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
// Find the slice from the root entity.
@@ -192,9 +223,14 @@ namespace AZ
SliceComponent::EntityList sliceEntities = sliceComponent->GetNewEntities();
AZ_Printf("Convert-Slice", " Slice contains %zu entities.\n", sliceEntities.size());
// Create the Prefab with the entities from the slice
// Create the Prefab with the entities from the slice.
// The entities are added in a separate step so that we can give them deterministic entity aliases that match their entity Ids
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> sourceInstance(
prefabSystemComponentInterface->CreatePrefab(sliceEntities, {}, outputPath));
prefabSystemComponentInterface->CreatePrefab({}, {}, outputPath));
for (auto& entity : sliceEntities)
{
sourceInstance->AddEntity(*entity, AZStd::string::format("Entity_%s", entity->GetId().ToString().c_str()));
}
// Dispatch events here, because prefab creation might trigger asset loads in rare circumstances.
AZ::Data::AssetManager::Instance().DispatchEvents();
@@ -204,12 +240,28 @@ namespace AZ
AzToolsFramework::Prefab::EntityOptionalReference container = sourceInstance->GetContainerEntity();
FixPrefabEntities(container->get(), sliceEntities);
// Keep track of the template Id we created, we're going to remove it at the end of slice file conversion to make sure
// the data doesn't stick around between file conversions.
auto templateId = sourceInstance->GetTemplateId();
if (templateId == AzToolsFramework::Prefab::InvalidTemplateId)
{
AZ_Printf("Convert-Slice", " Path error. Path could be invalid, or the prefab may not be loaded in this level.\n");
return false;
}
m_createdTemplateIds.emplace(templateId);
// Save off a mapping of the original slice entity IDs to the new prefab template entity aliases.
// When converting nested slices, this mapping will be needed to fix up the parent entity hierarchy correctly.
auto entityAliases = sourceInstance->GetEntityAliases();
for (auto& alias : entityAliases)
{
auto id = sourceInstance->GetEntityId(alias);
auto result = m_aliasIdMapper.emplace(TemplateEntityIdPair(templateId, id), alias);
if (!result.second)
{
AZ_Printf("Convert-Slice", " Duplicate entity alias -> entity id entries found, conversion may not be successful.\n");
}
}
// Update the prefab template with the fixed-up data in our prefab instance.
AzToolsFramework::Prefab::PrefabDom prefabDom;
@@ -254,21 +306,26 @@ namespace AZ
// via an EditorRequests EBus in CreatePrefab, but the subsystem that listens for it isn't present in this tool.)
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
&AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, containerEntity);
containerEntity.AddComponent(aznew AzToolsFramework::Prefab::EditorPrefabComponent());
if (containerEntity.FindComponent<AzToolsFramework::Prefab::EditorPrefabComponent>() == nullptr)
{
containerEntity.AddComponent(aznew AzToolsFramework::Prefab::EditorPrefabComponent());
}
// Make all the components on the container entity have deterministic component IDs, so that multiple runs of the tool
// on the same slice will produce the same prefab output. We're going to cheat a bit and just use the component type hash
// as the component ID. This would break if we had multiple components of the same type, but that currently doesn't
// happen for the container entity.
auto containerComponents = containerEntity.GetComponents();
for (auto& component : containerComponents)
{
component->SetId(component->GetUnderlyingComponentType().GetHash());
}
// Reparent any root-level slice entities to the container entity.
for (auto entity : sliceEntities)
{
AzToolsFramework::Components::TransformComponent* transformComponent =
entity->FindComponent<AzToolsFramework::Components::TransformComponent>();
if (transformComponent)
{
if (!transformComponent->GetParentId().IsValid())
{
transformComponent->SetParent(containerEntity.GetId());
transformComponent->UpdateCachedWorldTransform();
}
}
constexpr bool onlySetIfInvalid = true;
SetParentEntity(*entity, containerEntity.GetId(), onlySetIfInvalid);
}
}
@@ -276,9 +333,13 @@ namespace AZ
SliceComponent* sliceComponent, AzToolsFramework::Prefab::Instance* sourceInstance,
AZ::SerializeContext* serializeContext, bool isDryRun)
{
/* Given a root slice, find all the nested slices and convert them. */
// Get the list of nested slices that this slice uses.
const SliceComponent::SliceList& sliceList = sliceComponent->GetSlices();
auto prefabSystemComponentInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
// For each nested slice, convert it.
for (auto& slice : sliceList)
{
// Get the nested slice asset
@@ -312,7 +373,7 @@ namespace AZ
return false;
}
// Load the prefab template for the newly-created nested prefab.
// Find the prefab template we created for the newly-created nested prefab.
// To get the template, we need to take our absolute slice path and turn it into a project-relative prefab path.
AZ::IO::Path nestedPrefabPath = assetPath;
nestedPrefabPath.ReplaceExtension("prefab");
@@ -346,11 +407,25 @@ namespace AZ
}
bool SliceConverter::ConvertSliceInstance(
[[maybe_unused]] AZ::SliceComponent::SliceInstance& instance,
[[maybe_unused]] AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
AZ::SliceComponent::SliceInstance& instance,
AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
AzToolsFramework::Prefab::TemplateReference nestedTemplate,
AzToolsFramework::Prefab::Instance* topLevelInstance)
{
/* To convert a slice instance, it's important to understand the similarities and differences between slices and prefabs.
* Both slices and prefabs have the concept of instances of a nested slice/prefab, where each instance can have its own
* set of changed data (transforms, component values, etc).
* For slices, the changed data comes from applying a DataPatch to an instantiated set of entities from the nested slice.
* From prefabs, the changed data comes from Json patches that are applied to the instantiated set of entities from the
* nested prefab. The prefab instance entities also have different IDs than the slice instance entities, so we'll need
* to remap some of them along the way.
* To get from one to the other, we'll need to do the following:
* - Instantiate the nested slice and nested prefab
* - Patch the nested slice instance and fix up the entity ID references
* - Replace the nested prefab instance entities with the fixed-up slice ones
* - Add the nested instance (and the link patch) to the top-level prefab
*/
auto instanceToTemplateInterface = AZ::Interface<AzToolsFramework::Prefab::InstanceToTemplateInterface>::Get();
auto prefabSystemComponentInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
@@ -371,22 +446,83 @@ namespace AZ
AzToolsFramework::Prefab::PrefabDom unmodifiedNestedInstanceDom;
instanceToTemplateInterface->GenerateDomForInstance(unmodifiedNestedInstanceDom, *(nestedInstance.get()));
// Currently, DataPatch conversions for nested slices aren't implemented, so all nested slice overrides will
// be lost.
AZ_Warning(
"Convert-Slice", false, " Nested slice instances will lose all of their override data during conversion.",
nestedTemplate->get().GetFilePath().c_str());
// Instantiate a new instance of the nested slice
SliceComponent* dependentSlice = sliceAsset.Get()->GetComponent();
[[maybe_unused]] AZ::SliceComponent::InstantiateResult instantiationResult = dependentSlice->Instantiate();
AZ_Assert(instantiationResult == AZ::SliceComponent::InstantiateResult::Success, "Failed to instantiate instance");
// Set the container entity of the nested prefab to have the top-level prefab as the parent.
// Once DataPatch conversions are supported, this will need to change to nest the prefab under the appropriate entity
// within the level.
// Apply the data patch for this instance of the nested slice. This will provide us with a version of the slice's entities
// with all data overrides applied to them.
DataPatch::FlagsMap sourceDataFlags = dependentSlice->GetDataFlagsForInstances().GetDataFlagsForPatching();
DataPatch::FlagsMap targetDataFlags = instance.GetDataFlags().GetDataFlagsForPatching(&instance.GetEntityIdToBaseMap());
AZ::ObjectStream::FilterDescriptor filterDesc(AZ::Data::AssetFilterNoAssetLoading);
AZ::SliceComponent::InstantiatedContainer sourceObjects(false);
dependentSlice->GetEntities(sourceObjects.m_entities);
dependentSlice->GetAllMetadataEntities(sourceObjects.m_metadataEntities);
const DataPatch& dataPatch = instance.GetDataPatch();
auto instantiated =
dataPatch.Apply(&sourceObjects, dependentSlice->GetSerializeContext(), filterDesc, sourceDataFlags, targetDataFlags);
// Run through all the instantiated entities and fix up their parent hierarchy:
// - Invalid parents need to get set to the container.
// - Valid parents into the top-level instance mean that the nested slice instance is also child-nested under an entity.
// Prefabs handle this type of nesting differently - we need to set the parent to the container, and the container's
// parent to that other instance.
auto containerEntity = nestedInstance->GetContainerEntity();
AzToolsFramework::Components::TransformComponent* transformComponent =
containerEntity->get().FindComponent<AzToolsFramework::Components::TransformComponent>();
if (transformComponent)
auto containerEntityId = containerEntity->get().GetId();
for (auto entity : instantiated->m_entities)
{
transformComponent->SetParent(topLevelInstance->GetContainerEntityId());
transformComponent->UpdateCachedWorldTransform();
AzToolsFramework::Components::TransformComponent* transformComponent =
entity->FindComponent<AzToolsFramework::Components::TransformComponent>();
if (transformComponent)
{
bool onlySetIfInvalid = true;
auto parentId = transformComponent->GetParentId();
if (parentId.IsValid())
{
auto parentAlias = m_aliasIdMapper.find(TemplateEntityIdPair(topLevelInstance->GetTemplateId(), parentId));
if (parentAlias != m_aliasIdMapper.end())
{
// Set the container's parent to this entity's parent, and set this entity's parent to the container
// (i.e. go from A->B to A->container->B)
auto newParentId = topLevelInstance->GetEntityId(parentAlias->second);
SetParentEntity(containerEntity->get(), newParentId, false);
onlySetIfInvalid = false;
}
}
SetParentEntity(*entity, containerEntityId, onlySetIfInvalid);
}
}
// Replace all the entities in the instance with the new patched ones.
// (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us)
nestedInstance->RemoveNestedEntities(
[](const AZStd::unique_ptr<AZ::Entity>&)
{
return true;
});
for (auto& entity : instantiated->m_entities)
{
auto entityAlias = m_aliasIdMapper.find(TemplateEntityIdPair(nestedInstance->GetTemplateId(), entity->GetId()));
if (entityAlias != m_aliasIdMapper.end())
{
nestedInstance->AddEntity(*entity, entityAlias->second);
}
else
{
AZ_Assert(false, "Failed to find entity alias.");
nestedInstance->AddEntity(*entity);
}
}
// Set the container entity of the nested prefab to have the top-level prefab as the parent if it hasn't already gotten
// another entity as its parent.
{
constexpr bool onlySetIfInvalid = true;
SetParentEntity(containerEntity->get(), topLevelInstance->GetContainerEntityId(), onlySetIfInvalid);
}
// Add the nested instance itself to the top-level prefab. To do this, we need to add it to our top-level instance,
@@ -395,7 +531,22 @@ namespace AZ
AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomBefore;
instanceToTemplateInterface->GenerateDomForInstance(topLevelInstanceDomBefore, *topLevelInstance);
AzToolsFramework::Prefab::Instance& addedInstance = topLevelInstance->AddInstance(AZStd::move(nestedInstance));
// When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one
// will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce
// the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains
// a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the
// unique instance ID.
AZStd::string instanceAlias;
auto entityIdMap = instance.GetEntityIdMap();
if (!entityIdMap.empty())
{
instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str());
}
else
{
instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str());
}
AzToolsFramework::Prefab::Instance& addedInstance = topLevelInstance->AddInstance(AZStd::move(nestedInstance), instanceAlias);
AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomAfter;
instanceToTemplateInterface->GenerateDomForInstance(topLevelInstanceDomAfter, *topLevelInstance);
@@ -418,9 +569,26 @@ namespace AZ
AzToolsFramework::Prefab::InvalidLinkId);
prefabSystemComponentInterface->PropagateTemplateChanges(topLevelInstance->GetTemplateId());
AZ::Interface<AzToolsFramework::Prefab::InstanceUpdateExecutorInterface>::Get()->UpdateTemplateInstancesInQueue();
return true;
}
void SliceConverter::SetParentEntity(const AZ::Entity& entity, const AZ::EntityId& parentId, bool onlySetIfInvalid)
{
AzToolsFramework::Components::TransformComponent* transformComponent =
entity.FindComponent<AzToolsFramework::Components::TransformComponent>();
if (transformComponent)
{
// Only set the parent if we didn't set the onlySetIfInvalid flag, or if we did and the parent is currently invalid
if (!onlySetIfInvalid || !transformComponent->GetParentId().IsValid())
{
transformComponent->SetParent(parentId);
transformComponent->UpdateCachedWorldTransform();
}
}
}
void SliceConverter::PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId)
{
auto prefabSystemComponentInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
@@ -39,24 +39,35 @@ namespace AZ
class SliceConverter : public Converter
{
public:
static bool ConvertSliceFiles(Application& application);
bool ConvertSliceFiles(Application& application);
private:
static bool ConnectToAssetProcessor();
static void DisconnectFromAssetProcessor();
using TemplateEntityIdPair = AZStd::pair<AzToolsFramework::Prefab::TemplateId, AZ::EntityId>;
static bool ConvertSliceFile(AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun);
static bool ConvertSliceToPrefab(
bool ConnectToAssetProcessor();
void DisconnectFromAssetProcessor();
bool ConvertSliceFile(AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun);
bool ConvertSliceToPrefab(
AZ::SerializeContext* serializeContext, AZ::IO::PathView outputPath, bool isDryRun, AZ::Entity* rootEntity);
static void FixPrefabEntities(AZ::Entity& containerEntity, SliceComponent::EntityList& sliceEntities);
static bool ConvertNestedSlices(
void FixPrefabEntities(AZ::Entity& containerEntity, SliceComponent::EntityList& sliceEntities);
bool ConvertNestedSlices(
SliceComponent* sliceComponent, AzToolsFramework::Prefab::Instance* sourceInstance,
AZ::SerializeContext* serializeContext, bool isDryRun);
static bool ConvertSliceInstance(
bool ConvertSliceInstance(
AZ::SliceComponent::SliceInstance& instance, AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
AzToolsFramework::Prefab::TemplateReference nestedTemplate, AzToolsFramework::Prefab::Instance* topLevelInstance);
static void PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId);
static bool SavePrefab(AZ::IO::PathView outputPath, AzToolsFramework::Prefab::TemplateId templateId);
void SetParentEntity(const AZ::Entity& entity, const AZ::EntityId& parentId, bool onlySetIfInvalid);
void PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId);
bool SavePrefab(AZ::IO::PathView outputPath, AzToolsFramework::Prefab::TemplateId templateId);
// Track all of the entity IDs created and the prefab entity aliases that map to them. This mapping is used
// with nested slice conversion to remap parent entity IDs to the correct prefab entity IDs.
AZStd::unordered_map<TemplateEntityIdPair, AzToolsFramework::Prefab::EntityAlias> m_aliasIdMapper;
// Track all of the created prefab template IDs on a slice conversion so that they can get removed at the end of the
// conversion for that file.
AZStd::unordered_set<AzToolsFramework::Prefab::TemplateId> m_createdTemplateIds;
};
} // namespace SerializeContextTools
} // namespace AZ
@@ -0,0 +1,65 @@
/*
* 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 <AzToolsFramework/Entity/EditorEntityContextComponent.h>
namespace AzToolsFramework
{
// This class is an inelegant workaround for use by the Slice Converter to selectively disable entity add/remove logic
// during slice conversion in the EditorEntityContextComponent. Specifically, the standard versions of these methods will
// attempt to activate the entities as they're added. This is both unnecessary and undesirable during slice conversion, since
// entity activation requires a lot of subsystems to be active and valid.
// Instead, by selectively disabling this logic, the entities can remain in an initialized state, which is sufficient for conversion,
// without requiring those extra subsystems.
// This problem also could have been solved by adding APIs to the EditorEntityContextComponent or the EntityContext, but there aren't
// any other known valid use cases for disabling this logic, so the extra APIs would simply encourage "bad behavior" by using them
// when they likely aren't necessary or desired.
class SliceConverterEditorEntityContextComponent
: public EditorEntityContextComponent
{
public:
AZ_COMPONENT(SliceConverterEditorEntityContextComponent, "{1CB0C38F-8E85-4422-91C6-E1F3B9B4B853}");
SliceConverterEditorEntityContextComponent() : EditorEntityContextComponent() {}
// Simple API to selectively disable this logic *only* when performing slice to prefab conversion.
static void DisableOnContextEntityLogic()
{
m_enableOnContextEntityLogic = false;
}
protected:
void OnContextEntitiesAdded([[maybe_unused]] const EntityList& entities) override
{
if (m_enableOnContextEntityLogic)
{
EditorEntityContextComponent::OnContextEntitiesAdded(entities);
}
}
void OnContextEntityRemoved([[maybe_unused]] const AZ::EntityId& id) override
{
if (m_enableOnContextEntityLogic)
{
EditorEntityContextComponent::OnContextEntityRemoved(id);
}
}
// By default, act just like the EditorEntityContextComponent
static inline bool m_enableOnContextEntityLogic = true;
};
} // namespace AzToolsFramework
+2 -1
View File
@@ -125,7 +125,8 @@ int main(int argc, char** argv)
}
else if (AZ::StringFunc::Equal("convert-slice", action.c_str()))
{
result = SliceConverter::ConvertSliceFiles(application);
SliceConverter sliceConverter;
result = sliceConverter.ConvertSliceFiles(application);
}
else
{
@@ -17,6 +17,7 @@ set(FILES
Dumper.h
Dumper.cpp
main.cpp
SliceConverterEditorEntityContextComponent.h
SliceConverter.h
SliceConverter.cpp
Utilities.h
@@ -70,6 +70,52 @@ namespace LUAEditor
, m_bIsModified(false)
, m_bIsBeingSaved(false)
, m_PresetLineAtOpen(1){}
// Copy constructor does not copy over open file handle
DocumentInfo(const DocumentInfo& other)
: m_assetId(other.m_assetId)
, m_scriptAsset(other.m_scriptAsset)
, m_assetName(other.m_assetName)
, m_displayName(other.m_displayName)
, m_lastKnownModTime(other.m_lastKnownModTime)
, m_sourceControlInfo(other.m_sourceControlInfo)
, m_bSourceControl_Ready(other.m_bSourceControl_Ready)
, m_bSourceControl_BusyGettingStats(other.m_bSourceControl_BusyGettingStats)
, m_bSourceControl_BusyRequestingEdit(other.m_bSourceControl_BusyRequestingEdit)
, m_bSourceControl_CanWrite(other.m_bSourceControl_CanWrite)
, m_bSourceControl_CanCheckOut(other.m_bSourceControl_CanCheckOut)
, m_bDataIsLoaded(other.m_bDataIsLoaded)
, m_bDataIsWritten(other.m_bDataIsWritten)
, m_bCloseAfterSave(other.m_bCloseAfterSave)
, m_bUntitledDocument(other.m_bUntitledDocument)
, m_bIsModified(other.m_bIsModified)
, m_bIsBeingSaved(other.m_bIsBeingSaved)
, m_PresetLineAtOpen(other.m_PresetLineAtOpen)
{}
DocumentInfo& operator=(const DocumentInfo& other)
{
m_assetId = other.m_assetId;
m_scriptAsset = other.m_scriptAsset;
m_assetName = other.m_assetName;
m_displayName = other.m_displayName;
m_lastKnownModTime = other.m_lastKnownModTime;
m_sourceControlInfo = other.m_sourceControlInfo;
m_bSourceControl_Ready = other.m_bSourceControl_Ready;
m_bSourceControl_BusyGettingStats = other.m_bSourceControl_BusyGettingStats;
m_bSourceControl_BusyRequestingEdit = other.m_bSourceControl_BusyRequestingEdit;
m_bSourceControl_CanWrite = other.m_bSourceControl_CanWrite;
m_bSourceControl_CanCheckOut = other.m_bSourceControl_CanCheckOut;
m_bDataIsLoaded = other.m_bDataIsLoaded;
m_bDataIsWritten = other.m_bDataIsWritten;
m_bCloseAfterSave = other.m_bCloseAfterSave;
m_bUntitledDocument = other.m_bUntitledDocument;
m_bIsModified = other.m_bIsModified;
m_bIsBeingSaved = other.m_bIsBeingSaved;
m_PresetLineAtOpen = other.m_PresetLineAtOpen;
return *this;
}
};
class ContextInterface
@@ -1,14 +0,0 @@
/*
* 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 "StandaloneTools_precompiled.h"
@@ -10,7 +10,6 @@
#
set(FILES
StandaloneTools_precompiled.cpp
StandaloneTools_precompiled.h
targetver.h
Source/StandaloneToolsApplication.cpp