You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Editor/Plugins/ProjectSettingsTool/ProjectSettingsToolWindow.cpp

686 lines
24 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <EditorDefs.h>
#include "ProjectSettingsToolWindow.h"
#include "ui_ProjectSettingsToolWidget.h"
#include "DefaultImageValidator.h"
#include "PlatformSettings.h"
#include "ProjectSettingsContainer.h"
#include "ProjectSettingsValidator.h"
#include "PropertyImagePreview.h"
#include "PropertyFileSelect.h"
#include "PropertyLinked.h"
#include "Utils.h"
#include "ValidationHandler.h"
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/IO/Path/Path.h>
#include <AzToolsFramework/UI/PropertyEditor/InstanceDataHierarchy.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyManagerComponent.h>
#include <AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx>
#include <AzToolsFramework/UI/UICore/WidgetHelpers.h>
#include <QMessageBox>
#include <QCloseEvent>
#include <QScrollBar>
#include <QTimer>
// The object name in json for android
const static char* androidSettings = "android_settings";
static bool g_serializeRegistered = false;
namespace ProjectSettingsTool
{
using XmlNode = AZ::rapidxml::xml_node<char>;
ProjectSettingsToolWindow::ProjectSettingsToolWindow(QWidget* parent)
: QWidget(parent)
, LastPathBus::Handler()
, m_ui(new Ui::ProjectSettingsToolWidget())
, m_reconfigureProcess()
, m_projectRoot(GetProjectRoot())
, m_projectName(GetProjectName())
, m_plistsInitVector(
PlatformEnabled(PlatformId::Ios) ?
ProjectSettingsContainer::PlistInitVector({
ProjectSettingsContainer::PlatformAndPath
{ PlatformId::Ios, GetPlatformResource(PlatformId::Ios) }
})
:
ProjectSettingsContainer::PlistInitVector())
, m_settingsContainer(AZStd::make_unique<ProjectSettingsContainer>(m_projectRoot + "/project.json", m_plistsInitVector))
, m_validator(AZStd::make_unique<Validator>())
, m_platformProperties()
, m_platformPropertyEditors()
, m_propertyHandlers()
, m_validationHandler(AZStd::make_unique<ValidationHandler>())
, m_linkHandler(nullptr)
// The default path to select images at
, m_lastImagesPath(QStringLiteral("%1Code%2/Resources")
.arg(m_projectRoot.c_str()
, m_projectName.c_str()))
, m_invalidState(false)
{
// Shows any and all errors that occurred during serialization with option to quit out on each one.
ShowAllErrorsThenExitIfInvalid();
if (!g_serializeRegistered)
{
ReflectPlatformClasses();
g_serializeRegistered = true;
}
InitializeUi();
RegisterHandlersAndBusses();
AddAllPlatformsToUi();
MakeSerializers();
if (m_invalidState)
{
// Exit for safety
return;
}
LoadPropertiesFromSettings();
m_linkHandler->LinkAllProperties();
// Hide the iOS tab if that platform is not enabled.
if (!PlatformEnabled(PlatformId::Ios))
{
m_ui->platformTabs->removeTab(m_ui->platformTabs->indexOf(m_ui->iosTab));
}
}
ProjectSettingsToolWindow::~ProjectSettingsToolWindow()
{
UnregisterHandlersAndBusses();
}
QString ProjectSettingsToolWindow::GetLastImagePath()
{
return m_lastImagesPath;
}
void ProjectSettingsToolWindow::SetLastImagePath(const QString& path)
{
m_lastImagesPath = path;
}
FunctorValidator* ProjectSettingsToolWindow::GetValidator(FunctorValidator::FunctorType functor)
{
return m_validator->GetQValidator(functor);
}
void ProjectSettingsToolWindow::TrackValidator(FunctorValidator* validator)
{
m_validator->TrackThisValidator(validator);
}
void ProjectSettingsToolWindow::ReflectPlatformClasses()
{
AZ::SerializeContext* context = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext);
BaseSettings::Reflect(context);
AndroidSettings::Reflect(context);
IosSettings::Reflect(context);
}
void ProjectSettingsToolWindow::RegisterHandlersAndBusses()
{
m_propertyHandlers.push_back(PropertyFuncValLineEditHandler::Register(m_validationHandler.get()));
m_propertyHandlers.push_back(PropertyFuncValBrowseEditHandler::Register(m_validationHandler.get()));
m_propertyHandlers.push_back(PropertyFileSelectHandler::Register(m_validationHandler.get()));
m_propertyHandlers.push_back(PropertyImagePreviewHandler::Register(m_validationHandler.get()));
m_linkHandler = PropertyLinkedHandler::Register(m_validationHandler.get());
m_propertyHandlers.push_back(m_linkHandler);
LastPathBus::Handler::BusConnect();
ValidatorBus::Handler::BusConnect();
}
void ProjectSettingsToolWindow::UnregisterHandlersAndBusses()
{
ValidatorBus::Handler::BusDisconnect();
LastPathBus::Handler::BusDisconnect();
for (AzToolsFramework::PropertyHandlerBase* handler : m_propertyHandlers)
{
AzToolsFramework::PropertyTypeRegistrationMessages::Bus::Broadcast(
&AzToolsFramework::PropertyTypeRegistrationMessages::Bus::Handler::UnregisterPropertyType,
handler);
delete handler;
}
}
void ProjectSettingsToolWindow::MakeSerializerJson(const Platform& plat, AzToolsFramework::InstanceDataHierarchy& hierarchy, rapidjson::Document* doc)
{
m_platformSerializers[static_cast<unsigned>(plat.m_id)] = AZStd::make_unique<Serializer>(hierarchy.GetRoot(), doc);
}
void ProjectSettingsToolWindow::MakeSerializerJsonNonRoot(const Platform& plat, AzToolsFramework::InstanceDataHierarchy& hierarchy, rapidjson::Document* doc, rapidjson::Value* jsonRoot)
{
m_platformSerializers[static_cast<unsigned>(plat.m_id)] = AZStd::make_unique<Serializer>(hierarchy.GetRoot(), doc, jsonRoot);
}
void ProjectSettingsToolWindow::MakeSerializerPlist(const Platform& plat, AzToolsFramework::InstanceDataHierarchy& hierarchy, PlistDictionary* dict)
{
m_platformSerializers[static_cast<unsigned>(plat.m_id)] = AZStd::make_unique<Serializer>(hierarchy.GetRoot(), AZStd::unique_ptr<PlistDictionary>(dict));
}
void ProjectSettingsToolWindow::closeEvent(QCloseEvent* event)
{
if (!m_invalidState)
{
// Check if ui is loaded
if (m_ui->saveButton != nullptr)
{
// Save button is used as an inverse bool to tell if configure is being run or settings are being saved
if (m_ui->saveButton->isEnabled())
{
if (!UiEqualToSettings())
{
int result = QMessageBox::question
(
this,
tr("Warning"),
tr("There are currently unsaved changes. Are you sure you want to cancel?"),
QMessageBox::Yes,
QMessageBox::No
);
if (QMessageBox::Yes == result)
{
QWidget::closeEvent(event);
}
else
{
event->setAccepted(false);
}
}
else
{
QWidget::closeEvent(event);
}
}
else
{
QMessageBox::information
(
this,
tr("Info"),
tr("Cannot close until settings have been reconfigured."),
QMessageBox::Ok
);
event->setAccepted(false);
}
}
else
{
QWidget::closeEvent(event);
}
}
else
{
QWidget::closeEvent(event);
}
}
void ProjectSettingsToolWindow::ForceClose()
{
m_invalidState = true;
// Potentially called from the constructor, when the widget/window aren't properly set up, so delay this until after it's all setup
QTimer::singleShot(0, this, [this]() {window()->close();} );
}
bool ProjectSettingsToolWindow::IfErrorShowThenExit()
{
// Grabs the earliest unseen error popping it off the error queue
AZ::Outcome<void, SettingsError> error = m_settingsContainer->GetError();
if (!error.IsSuccess())
{
bool shouldAbort = error.GetError().m_error == m_settingsContainer->GetFailedLoadingPlistText();
QMessageBox::StandardButton result = QMessageBox::critical
(
this,
error.GetError().m_error.c_str(),
error.GetError().m_reason.c_str(),
shouldAbort ? QMessageBox::Abort : QMessageBox::StandardButtons(QMessageBox::Ok | QMessageBox::Abort),
shouldAbort ? QMessageBox::Abort : QMessageBox::Ok
);
if (result == QMessageBox::Abort)
{
ForceClose();
}
return true;
}
return false;
}
void ProjectSettingsToolWindow::ShowAllErrorsThenExitIfInvalid()
{
while (IfErrorShowThenExit())
{
if (m_invalidState)
{
// Exit for safety
return;
}
}
}
void ProjectSettingsToolWindow::InitializeUi()
{
// setup
m_ui->setupUi(this);
AzQtComponents::TabWidget::applySecondaryStyle(m_ui->platformTabs, false);
ResizeTabs(m_ui->platformTabs->currentIndex());
m_ui->reconfigureLog->hide();
// connects
connect
(
&m_reconfigureProcess,
static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
this,
[this]()
{
m_ui->saveButton->setEnabled(true);
m_ui->reloadButton->setEnabled(true);
m_ui->reconfigureLog->insertPlainText(tr("\n Reconfiguration Finished"));
QScrollBar* scrollbar = m_ui->reconfigureLog->verticalScrollBar();
scrollbar->setValue(scrollbar->maximum());
}
);
connect(&m_reconfigureProcess, &QProcess::readyReadStandardOutput, this,
[this]()
{
m_ui->reconfigureLog->insertPlainText(m_reconfigureProcess.readAllStandardOutput());
if (!m_ui->reconfigureLog->textCursor().hasSelection())
{
QScrollBar* scrollbar = m_ui->reconfigureLog->verticalScrollBar();
scrollbar->setValue(scrollbar->maximum());
}
});
connect(&m_reconfigureProcess, &QProcess::readyReadStandardError, this,
[this]()
{
m_ui->reconfigureLog->insertPlainText(m_reconfigureProcess.readAllStandardError());
if (!m_ui->reconfigureLog->textCursor().hasSelection())
{
QScrollBar* scrollbar = m_ui->reconfigureLog->verticalScrollBar();
scrollbar->setValue(scrollbar->maximum());
}
});
connect(m_ui->platformTabs, &QTabWidget::currentChanged,
this, &ProjectSettingsToolWindow::ResizeTabs);
connect(m_ui->saveButton, &QPushButton::clicked, this,
&ProjectSettingsToolWindow::SaveSettingsFromUi);
connect(m_ui->reloadButton, &QPushButton::clicked, this,
&ProjectSettingsToolWindow::ReloadUiFromSettings);
}
void ProjectSettingsToolWindow::ResizeTabs(int index)
{
for (int i = 0; i < m_ui->platformTabs->count(); i++)
{
if (i != index)
{
m_ui->platformTabs->widget(i)->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
}
}
// resize for current tab
m_ui->platformTabs->widget(index)->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
m_ui->platformTabs->widget(index)->resize(m_ui->platformTabs->widget(index)->minimumSizeHint());
m_ui->platformTabs->widget(index)->adjustSize();
}
void ProjectSettingsToolWindow::AddAllPlatformsToUi()
{
for (int plat = 0; plat < static_cast<unsigned long long>(PlatformId::NumPlatformIds); ++plat)
{
AddPlatformToUi(Platforms[plat]);
}
}
void ProjectSettingsToolWindow::AddPlatformToUi(const Platform& plat)
{
AZ::SerializeContext* context = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext);
QWidget* parent = nullptr;
void* dataForPropertyEditor = nullptr;
AZ::TypeId dataTypeID;
switch (plat.m_id)
{
case PlatformId::Base:
parent = m_ui->baseSettingsGroupBox;
dataForPropertyEditor = &m_platformProperties.base;
dataTypeID = m_platformProperties.base.TYPEINFO_Uuid();
break;
case PlatformId::Android:
parent = m_ui->androidTab;
dataForPropertyEditor = &m_platformProperties.android;
dataTypeID = m_platformProperties.android.TYPEINFO_Uuid();
break;
case PlatformId::Ios:
parent = m_ui->iosTab;
dataForPropertyEditor = &m_platformProperties.ios;
dataTypeID = m_platformProperties.ios.TYPEINFO_Uuid();
break;
default:
AZ_Assert(false, "Cannot add unknown platform to ui.");
}
unsigned platIdValue = static_cast<unsigned>(plat.m_id);
m_platformPropertyEditors[platIdValue] = aznew AzToolsFramework::ReflectedPropertyEditor(parent);
parent->layout()->addWidget(m_platformPropertyEditors[platIdValue]);
m_platformPropertyEditors[platIdValue]->Setup(context, nullptr, false);
m_platformPropertyEditors[platIdValue]->AddInstance(dataForPropertyEditor, dataTypeID);
m_platformPropertyEditors[platIdValue]->setVisible(true);
m_platformPropertyEditors[platIdValue]->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
m_platformPropertyEditors[platIdValue]->SetHideRootProperties(false);
m_platformPropertyEditors[platIdValue]->SetDynamicEditDataProvider(nullptr);
m_platformPropertyEditors[platIdValue]->ExpandAll();
m_platformPropertyEditors[platIdValue]->InvalidateAll();
}
const char* GetPlatformKey(const Platform& plat)
{
switch (plat.m_id)
{
case PlatformId::Android:
return androidSettings;
default:
return "";
}
}
void ProjectSettingsToolWindow::MakeSerializers()
{
for (int plat = 0; plat < static_cast<unsigned long long>(PlatformId::NumPlatformIds); ++plat)
{
if (PlatformEnabled(static_cast<PlatformId>(plat)))
{
MakePlatformSerializer(Platforms[plat]);
}
}
}
void ProjectSettingsToolWindow::MakePlatformSerializer(const Platform& plat)
{
unsigned platIdValue = static_cast<unsigned>(plat.m_id);
switch (plat.m_id)
{
case PlatformId::Base:
m_platformPropertyEditors[platIdValue]->EnumerateInstances(AZStd::bind
(
&ProjectSettingsToolWindow::MakeSerializerJson,
this,
plat,
AZStd::placeholders::_1,
&m_settingsContainer->GetProjectJsonDocument()
));
break;
case PlatformId::Android:
m_platformPropertyEditors[platIdValue]->EnumerateInstances(AZStd::bind
(
&ProjectSettingsToolWindow::MakeSerializerJsonNonRoot,
this,
plat,
AZStd::placeholders::_1,
&m_settingsContainer->GetProjectJsonDocument(),
m_settingsContainer->GetProjectJsonValue(GetPlatformKey(plat)).GetValue()
));
break;
case PlatformId::Ios:
{
PlistDictionary* dict = m_settingsContainer->GetPlistDictionary(plat).release();
if (dict == nullptr)
{
QMessageBox::critical
(
this,
"Critical",
"Ios pList is invalid. Project Settings Tool must close.",
QMessageBox::Abort
);
ForceClose();
}
m_platformPropertyEditors[platIdValue]->EnumerateInstances(AZStd::bind
(
&ProjectSettingsToolWindow::MakeSerializerPlist,
this,
plat,
AZStd::placeholders::_1,
// All arguments must be copy constructible so this must be released
dict
));
break;
}
default:
AZ_Assert(false, "Cannot make serializer for unknown platform.");
break;
}
}
void ProjectSettingsToolWindow::LoadPropertiesFromSettings()
{
// Disable all fields links
for (int plat = 0; plat < static_cast<unsigned long long>(PlatformId::NumPlatformIds); ++plat)
{
if (PlatformEnabled(static_cast<PlatformId>(plat)))
{
LoadPropertiesFromPlatformSettings(Platforms[plat]);
}
}
}
void ProjectSettingsToolWindow::LoadPropertiesFromPlatformSettings(const Platform& plat)
{
unsigned platIdValue = static_cast<unsigned>(plat.m_id);
m_platformSerializers[platIdValue]->LoadFromSettings();
m_platformPropertyEditors[platIdValue]->InvalidateValues();
}
bool ProjectSettingsToolWindow::UiEqualToSettings()
{
for (int plat = 0; plat < static_cast<unsigned long long>(PlatformId::NumPlatformIds); ++plat)
{
if (PlatformEnabled(static_cast<PlatformId>(plat)))
{
if (!UiEqualToPlatformSettings(Platforms[plat]))
{
return false;
}
}
}
return true;
}
bool ProjectSettingsToolWindow::UiEqualToPlatformSettings(const Platform& plat)
{
return m_platformSerializers[static_cast<unsigned>(plat.m_id)]->UiEqualToSettings();
}
bool ProjectSettingsToolWindow::ValidateAllProperties()
{
return m_validationHandler->AllValid();
}
void ProjectSettingsToolWindow::SaveSettingsFromUi()
{
bool anySaves = false;
const unsigned long long numPlatforms = static_cast<unsigned long long>(PlatformId::NumPlatformIds);
bool needToSavePlat[numPlatforms] = {false};
for (int plat = 0; plat < numPlatforms; ++plat)
{
if (PlatformEnabled(static_cast<PlatformId>(plat)))
{
const Platform& platform = Platforms[plat];
if (!UiEqualToPlatformSettings(platform))
{
needToSavePlat[plat] = true;
anySaves = true;
}
}
}
if (anySaves)
{
// Keeps queued button presses from getting in
if (m_ui->saveButton->isEnabled())
{
m_ui->saveButton->setEnabled(false);
m_ui->reloadButton->setEnabled(false);
if (ValidateAllProperties())
{
bool projectJsonChanged = false;
for (int plat = 0; plat < numPlatforms; ++plat)
{
const Platform& platform = Platforms[plat];
if (needToSavePlat[plat])
{
m_platformSerializers[plat]->SaveToSettings();
if (m_settingsContainer->IsPlistPlatform(platform))
{
m_settingsContainer->SavePlistData(platform);
}
else
{
projectJsonChanged = true;
}
}
}
if (projectJsonChanged)
{
m_settingsContainer->SaveProjectJsonData();
}
ShowAllErrorsThenExitIfInvalid();
m_ui->reconfigureLog->setText("");
QMessageBox::information(this, tr("Project Settings Saved"),
tr("The project may need to be manually reconfigured for the new settings to be applied."));
m_ui->reloadButton->setEnabled(true);
m_ui->saveButton->setEnabled(true);
}
// Show a message box telling user settings failed to save
else
{
QMessageBox::critical(this, tr("Failed To Save"), tr("Failed to save due to invalid settings."));
m_ui->reloadButton->setEnabled(true);
m_ui->saveButton->setEnabled(true);
}
}
}
}
void ProjectSettingsToolWindow::SaveSettingsFromPlatformUi(const Platform& plat)
{
m_platformSerializers[static_cast<unsigned>(plat.m_id)]->SaveToSettings();
m_settingsContainer->SavePlatformData(plat);
ShowAllErrorsThenExitIfInvalid();
}
void ProjectSettingsToolWindow::ReloadUiFromSettings()
{
if (!UiEqualToSettings())
{
int result = QMessageBox::warning
(
this,
tr("Reload Settings"),
tr("Are you sure you would like to reload settings from file? All changes will be lost."),
QMessageBox::Reset,
QMessageBox::Cancel);
if (result == QMessageBox::Reset)
{
m_settingsContainer->ReloadProjectJsonData();
m_settingsContainer->ReloadPlistData();
MakeSerializers();
// Disable links to avoid overwriting values while loading
m_linkHandler->DisableAllPropertyLinks();
LoadPropertiesFromSettings();
// Re-enable them then mirror
m_linkHandler->EnableAllPropertyLinks();
m_linkHandler->EnableOptionalLinksIfAllPropertiesEqual();
m_linkHandler->MirrorAllLinkedProperties();
// Mark any invalid fields loaded from file
ValidateAllProperties();
}
}
}
bool ProjectSettingsToolWindow::PlatformEnabled(PlatformId platformId)
{
// iOS can be disabled if the plist file is missing
if (platformId == PlatformId::Ios)
{
AZStd::string plistPath = GetPlatformResource(platformId);
return !plistPath.empty();
}
return true;
}
AZStd::string ProjectSettingsToolWindow::GetPlatformResource(PlatformId platformId)
{
if (platformId == PlatformId::Ios)
{
const char* searchPaths[] = {
"Resources/Platform/iOS/Info.plist",
// legacy paths
"Gem/Resources/Platform/iOS/Info.plist",
"Gem/Resources/IOSLauncher/Info.plist",
};
for (auto relPath : searchPaths)
{
AZ::IO::FixedMaxPath projectPlist{ m_projectRoot };
projectPlist /= relPath;
if (AZ::IO::SystemFile::Exists(projectPlist.c_str()))
{
return projectPlist.LexicallyNormal().String();
}
}
}
return AZStd::string();
}
#include <moc_ProjectSettingsToolWindow.cpp>
} // namespace ProjectSettingsTool