Merge remote-tracking branch 'upstream/development' into Atom/rbarrand/MaterialVersionUpdate

This commit is contained in:
santorac
2021-10-21 11:14:03 -07:00
559 changed files with 13714 additions and 648228 deletions
+18 -16
View File
@@ -287,21 +287,22 @@ bool CCryDocManager::DoPromptFileName(QString& fileName, [[maybe_unused]] UINT n
return false;
}
CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAddToMRU)
CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions)
{
assert(lpszFileName != nullptr);
assert(filename != nullptr);
const bool reopenIfSame = openSameLevelOptions == COpenSameLevelOptions::ReopenLevelIfSame;
// find the highest confidence
auto pos = m_templateList.begin();
CCrySingleDocTemplate::Confidence bestMatch = CCrySingleDocTemplate::noAttempt;
CCrySingleDocTemplate* pBestTemplate = nullptr;
CCryEditDoc* pOpenDocument = nullptr;
if (lpszFileName[0] == '\"')
if (filename[0] == '\"')
{
++lpszFileName;
++filename;
}
QString szPath = QString::fromUtf8(lpszFileName);
QString szPath = QString::fromUtf8(filename);
if (szPath.endsWith('"'))
{
szPath.remove(szPath.length() - 1, 1);
@@ -325,7 +326,7 @@ CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAd
}
}
if (pOpenDocument != nullptr)
if (!reopenIfSame && pOpenDocument != nullptr)
{
return pOpenDocument;
}
@@ -336,7 +337,7 @@ CCryEditDoc* CCryDocManager::OpenDocumentFile(const char* lpszFileName, bool bAd
return nullptr;
}
return pBestTemplate->OpenDocumentFile(szPath.toUtf8().data(), bAddToMRU, false);
return pBestTemplate->OpenDocumentFile(szPath.toUtf8().data(), addToMostRecentFileList, false);
}
//////////////////////////////////////////////////////////////////////////////
@@ -513,7 +514,7 @@ public:
QString m_appRoot;
QString m_logFile;
QString m_pythonArgs;
QString m_pythontTestCase;
QString m_pythonTestCase;
QString m_execFile;
QString m_execLineCmd;
@@ -562,7 +563,7 @@ public:
const std::vector<std::pair<CommandLineStringOption, QString&> > stringOptions = {
{{"logfile", "File name of the log file to write out to.", "logfile"}, m_logFile},
{{"runpythonargs", "Command-line argument string to pass to the python script if --runpython or --runpythontest was used.", "runpythonargs"}, m_pythonArgs},
{{"pythontestcase", "Test case name of python test script if --runpythontest was used.", "pythontestcase"}, m_pythontTestCase},
{{"pythontestcase", "Test case name of python test script if --runpythontest was used.", "pythontestcase"}, m_pythonTestCase},
{{"exec", "cfg file to run on startup, used for systems like automation", "exec"}, m_execFile},
{{"rhi", "Command-line argument to force which rhi to use", "dummyString"}, dummyString },
{{"rhi-device-validation", "Command-line argument to configure rhi validation", "dummyString"}, dummyString },
@@ -818,7 +819,7 @@ CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, b
return OpenDocumentFile(lpszPathName, true, bMakeVisible);
}
CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, bool bAddToMRU, [[maybe_unused]] bool bMakeVisible)
CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, bool addToMostRecentFileList, [[maybe_unused]] bool bMakeVisible)
{
CCryEditDoc* pCurDoc = GetIEditor()->GetDocument();
@@ -848,7 +849,7 @@ CCryEditDoc* CCrySingleDocTemplate::OpenDocumentFile(const char* lpszPathName, b
{
pCurDoc->OnOpenDocument(lpszPathName);
pCurDoc->SetPathName(lpszPathName);
if (bAddToMRU)
if (addToMostRecentFileList)
{
CCryEditApp::instance()->AddToRecentFileList(lpszPathName);
}
@@ -1535,11 +1536,12 @@ void CCryEditApp::RunInitPythonScript(CEditCommandLineInfo& cmdInfo)
{
// Multiple testcases can be specified them with ';', these should match the files to run
AZStd::vector<AZStd::string_view> testcaseList;
QByteArray pythonTestCase = cmdInfo.m_pythonTestCase.toUtf8();
testcaseList.resize(fileList.size());
{
int i = 0;
AzFramework::StringFunc::TokenizeVisitor(
fileStr.constData(),
pythonTestCase.constData(),
[&i, &testcaseList](AZStd::string_view elem)
{
testcaseList[i++] = (elem);
@@ -2630,7 +2632,7 @@ void CCryEditApp::OnShowHelpers()
void CCryEditApp::OnEditLevelData()
{
auto dir = QFileInfo(GetIEditor()->GetDocument()->GetLevelPathName()).dir();
CFileUtil::EditTextFile(dir.absoluteFilePath("LevelData.xml").toUtf8().data());
CFileUtil::EditTextFile(dir.absoluteFilePath("leveldata.xml").toUtf8().data());
}
//////////////////////////////////////////////////////////////////////////
@@ -3364,7 +3366,7 @@ void CCryEditApp::OnOpenSlice()
}
//////////////////////////////////////////////////////////////////////////
CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* lpszFileName)
CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions)
{
if (m_openingLevel)
{
@@ -3404,9 +3406,9 @@ CCryEditDoc* CCryEditApp::OpenDocumentFile(const char* lpszFileName)
openDocTraceHandler.SetShowWindow(false);
}
// in this case, we set bAddToMRU to always be true because adding files to the MRU list
// in this case, we set addToMostRecentFileList to always be true because adding files to the MRU list
// automatically culls duplicate and normalizes paths anyway
m_pDocManager->OpenDocumentFile(lpszFileName, true);
m_pDocManager->OpenDocumentFile(filename, addToMostRecentFileList, openSameLevelOptions);
if (openDocTraceHandler.HasAnyErrors())
{
+11 -3
View File
@@ -85,6 +85,12 @@ public:
using EditorIdleProcessingBus = AZ::EBus<EditorIdleProcessing>;
enum class COpenSameLevelOptions
{
ReopenLevelIfSame,
NotReopenIfSame
};
AZ_PUSH_DISABLE_DLL_EXPORT_BASECLASS_WARNING
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
class SANDBOX_API CCryEditApp
@@ -174,7 +180,9 @@ public:
virtual bool InitInstance();
virtual int ExitInstance(int exitCode = 0);
virtual bool OnIdle(LONG lCount);
virtual CCryEditDoc* OpenDocumentFile(const char* lpszFileName);
virtual CCryEditDoc* OpenDocumentFile(const char* filename,
bool addToMostRecentFileList=true,
COpenSameLevelOptions openSameLevelOptions = COpenSameLevelOptions::NotReopenIfSame);
CCryDocManager* GetDocManager() { return m_pDocManager; }
@@ -448,7 +456,7 @@ public:
~CCrySingleDocTemplate() {};
// avoid creating another CMainFrame
// close other type docs before opening any things
virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool bAddToMRU, bool bMakeVisible);
virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool addToMostRecentFileList, bool bMakeVisible);
virtual CCryEditDoc* OpenDocumentFile(const char* lpszPathName, bool bMakeVisible = TRUE);
virtual Confidence MatchDocType(const char* lpszPathName, CCryEditDoc*& rpDocMatch);
@@ -468,7 +476,7 @@ public:
virtual void OnFileNew();
virtual bool DoPromptFileName(QString& fileName, UINT nIDSTitle,
DWORD lFlags, bool bOpenFileDialog, CDocTemplate* pTemplate);
virtual CCryEditDoc* OpenDocumentFile(const char* lpszFileName, bool bAddToMRU);
virtual CCryEditDoc* OpenDocumentFile(const char* filename, bool addToMostRecentFileList, COpenSameLevelOptions openSameLevelOptions = COpenSameLevelOptions::NotReopenIfSame);
QVector<CCrySingleDocTemplate*> m_templateList;
};
+11 -13
View File
@@ -143,20 +143,11 @@ namespace
return false;
}
}
const bool addToMostRecentFileList = false;
auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data(),
addToMostRecentFileList, COpenSameLevelOptions::ReopenLevelIfSame);
auto previousDocument = GetIEditor()->GetDocument();
QString previousPathName = (previousDocument != nullptr) ? previousDocument->GetLevelPathName() : "";
auto newDocument = CCryEditApp::instance()->OpenDocumentFile(levelPath.toUtf8().data());
// the underlying document pointer doesn't change, so we can't check that; use the path name's instead
bool result = true;
if (newDocument == nullptr || newDocument->IsLevelLoadFailed() || (newDocument->GetLevelPathName() == previousPathName))
{
result = false;
}
return result;
return newDocument != nullptr && !newDocument->IsLevelLoadFailed();
}
bool PyOpenLevelNoPrompt(const char* pLevelName)
@@ -407,6 +398,11 @@ inline namespace Commands
{
return AZ::Debug::Trace::WaitForDebugger(timeoutSeconds);
}
AZStd::string PyGetFileAlias(AZStd::string alias)
{
return AZ::IO::FileIOBase::GetInstance()->GetAlias(alias.c_str());
}
}
namespace AzToolsFramework
@@ -457,6 +453,8 @@ namespace AzToolsFramework
addLegacyGeneral(behaviorContext->Method("attach_debugger", PyAttachDebugger, nullptr, "Prompts for attaching the debugger"));
addLegacyGeneral(behaviorContext->Method("wait_for_debugger", PyWaitForDebugger, behaviorContext->MakeDefaultValues(-1.f), "Pauses this thread execution until the debugger has been attached"));
addLegacyGeneral(behaviorContext->Method("get_file_alias", PyGetFileAlias, nullptr, "Retrieves path for IO alias"));
// this will put these methods into the 'azlmbr.legacy.checkout_dialog' module
auto addCheckoutDialog = [](AZ::BehaviorContext::GlobalMethodBuilder methodBuilder)
{
@@ -75,7 +75,10 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("CaptureCursorLook", &CameraMovementSettings::m_captureCursorLook)
->Field("OrbitYawRotationInverted", &CameraMovementSettings::m_orbitYawRotationInverted)
->Field("PanInvertedX", &CameraMovementSettings::m_panInvertedX)
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY);
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY)
->Field("DefaultPositionX", &CameraMovementSettings::m_defaultCameraPositionX)
->Field("DefaultPositionY", &CameraMovementSettings::m_defaultCameraPositionY)
->Field("DefaultPositionZ", &CameraMovementSettings::m_defaultCameraPositionZ);
serialize.Class<CameraInputSettings>()
->Version(2)
@@ -154,7 +157,16 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
"Invert direction of pan in local Y axis")
->DataElement(
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_captureCursorLook, "Camera Capture Look Cursor",
"Should the cursor be captured (hidden) while performing free look");
"Should the cursor be captured (hidden) while performing free look")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionX, "Default X Camera Position",
"Default X Camera Position when a level is opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionY, "Default Y Camera Position",
"Default Y Camera Position when a level is opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionZ, "Default Z Camera Position",
"Default Z Camera Position when a level is opened");
editContext->Class<CameraInputSettings>("Camera Input Settings", "")
->DataElement(
@@ -271,6 +283,12 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraOrbitYawRotationInverted(m_cameraMovementSettings.m_orbitYawRotationInverted);
SandboxEditor::SetCameraPanInvertedX(m_cameraMovementSettings.m_panInvertedX);
SandboxEditor::SetCameraPanInvertedY(m_cameraMovementSettings.m_panInvertedY);
SandboxEditor::SetDefaultCameraEditorPosition(
AZ::Vector3(
m_cameraMovementSettings.m_defaultCameraPositionX,
m_cameraMovementSettings.m_defaultCameraPositionY,
m_cameraMovementSettings.m_defaultCameraPositionZ
));
SandboxEditor::SetCameraTranslateForwardChannelId(m_cameraInputSettings.m_translateForwardChannelId);
SandboxEditor::SetCameraTranslateBackwardChannelId(m_cameraInputSettings.m_translateBackwardChannelId);
@@ -308,6 +326,11 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraMovementSettings.m_panInvertedX = SandboxEditor::CameraPanInvertedX();
m_cameraMovementSettings.m_panInvertedY = SandboxEditor::CameraPanInvertedY();
AZ::Vector3 defaultCameraPosition = SandboxEditor::DefaultEditorCameraPosition();
m_cameraMovementSettings.m_defaultCameraPositionX = defaultCameraPosition.GetX();
m_cameraMovementSettings.m_defaultCameraPositionY = defaultCameraPosition.GetY();
m_cameraMovementSettings.m_defaultCameraPositionZ = defaultCameraPosition.GetZ();
m_cameraInputSettings.m_translateForwardChannelId = SandboxEditor::CameraTranslateForwardChannelId().GetName();
m_cameraInputSettings.m_translateBackwardChannelId = SandboxEditor::CameraTranslateBackwardChannelId().GetName();
m_cameraInputSettings.m_translateLeftChannelId = SandboxEditor::CameraTranslateLeftChannelId().GetName();
@@ -57,6 +57,9 @@ private:
bool m_orbitYawRotationInverted;
bool m_panInvertedX;
bool m_panInvertedY;
float m_defaultCameraPositionX;
float m_defaultCameraPositionY;
float m_defaultCameraPositionZ;
AZ::Crc32 RotateSmoothingVisibility() const
{
+18
View File
@@ -52,6 +52,9 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraOrbitDollyIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitDollyId";
constexpr AZStd::string_view CameraOrbitPanIdSetting = "/Amazon/Preferences/Editor/Camera/OrbitPanId";
constexpr AZStd::string_view CameraFocusIdSetting = "/Amazon/Preferences/Editor/Camera/FocusId";
constexpr AZStd::string_view CameraDefaultStartingPositionX = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/x";
constexpr AZStd::string_view CameraDefaultStartingPositionY = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/y";
constexpr AZStd::string_view CameraDefaultStartingPositionZ = "/Amazon/Preferences/Editor/Camera/DefaultStartingPosition/z";
template<typename T>
void SetRegistry(const AZStd::string_view setting, T&& value)
@@ -111,6 +114,21 @@ namespace SandboxEditor
return AZStd::make_unique<EditorViewportSettingsCallbacksImpl>();
}
AZ::Vector3 DefaultEditorCameraPosition()
{
float xPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0));
float yPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0));
float zPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0));
return AZ::Vector3(xPosition, yPosition, zPosition);
}
void SetDefaultCameraEditorPosition(const AZ::Vector3 defaultCameraPosition)
{
SetRegistry(CameraDefaultStartingPositionX, defaultCameraPosition.GetX());
SetRegistry(CameraDefaultStartingPositionY, defaultCameraPosition.GetY());
SetRegistry(CameraDefaultStartingPositionZ, defaultCameraPosition.GetZ());
}
AZ::u64 MaxItemsShownInAssetBrowserSearch()
{
return GetRegistry(AssetBrowserMaxItemsShownInSearchSetting, aznumeric_cast<AZ::u64>(50));
+4
View File
@@ -12,6 +12,7 @@
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzCore/Math/Vector3.h>
#include <AzFramework/Input/Channels/InputChannelId.h>
namespace SandboxEditor
@@ -32,6 +33,9 @@ namespace SandboxEditor
//! event will fire when a value in the settings registry (editorpreferences.setreg) is modified.
SANDBOX_API AZStd::unique_ptr<EditorViewportSettingsCallbacks> CreateEditorViewportSettingsCallbacks();
SANDBOX_API AZ::Vector3 DefaultEditorCameraPosition();
SANDBOX_API void SetDefaultCameraEditorPosition(AZ::Vector3 defaultCameraPosition);
SANDBOX_API AZ::u64 MaxItemsShownInAssetBrowserSearch();
SANDBOX_API void SetMaxItemsShownInAssetBrowserSearch(AZ::u64 numberOfItemsShown);
+16 -22
View File
@@ -112,7 +112,6 @@ void StartFixedCursorMode(QObject *viewport);
#define RENDER_MESH_TEST_DISTANCE (0.2f)
#define CURSOR_FONT_HEIGHT 8.0f
namespace AZ::ViewportHelpers
{
static const char TextCantCreateCameraNoLevel[] = "Cannot create camera when no level is loaded.";
@@ -623,16 +622,10 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
PopDisableRendering();
{
AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
float sx = terrainAabb.GetXExtent();
float sy = terrainAabb.GetYExtent();
Matrix34 viewTM;
viewTM.SetIdentity();
// Initial camera will be at middle of the map at the height of 2
// meters above the terrain (default terrain height is 32)
viewTM.SetTranslation(Vec3(sx * 0.5f, sy * 0.5f, 34.0f));
viewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(viewTM);
UpdateScene();
@@ -647,16 +640,10 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
PopDisableRendering();
{
AZ::Aabb terrainAabb = AZ::Aabb::CreateFromPoint(AZ::Vector3::CreateZero());
AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult(terrainAabb, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb);
float sx = terrainAabb.GetXExtent();
float sy = terrainAabb.GetYExtent();
Matrix34 viewTM;
viewTM.SetIdentity();
// Initial camera will be at middle of the map at the height of 2
// meters above the terrain (default terrain height is 32)
viewTM.SetTranslation(Vec3(sx * 0.5f, sy * 0.5f, 34.0f));
viewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(viewTM);
}
break;
@@ -1345,10 +1332,6 @@ void EditorViewportWidget::keyPressEvent(QKeyEvent* event)
void EditorViewportWidget::SetViewTM(const Matrix34& tm)
{
if (m_viewSourceType == ViewSourceType::None)
{
m_defaultViewTM = tm;
}
SetViewTM(tm, false);
}
@@ -1445,6 +1428,10 @@ void EditorViewportWidget::SetViewTM(const Matrix34& camMatrix, bool bMoveOnly)
"Please report this as a bug."
);
}
else if (shouldUpdateObject == ShouldUpdateObject::No)
{
GetCurrentAtomView()->SetCameraTransform(LYTransformToAZMatrix3x4(camMatrix));
}
if (m_pressedKeyState == KeyPressedState::PressedThisFrame)
{
@@ -2028,6 +2015,9 @@ void EditorViewportWidget::SetDefaultCamera()
m_viewSourceType = ViewSourceType::None;
GetViewManager()->SetCameraObjectId(GUID_NULL);
SetName(m_defaultViewName);
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
// Synchronize the configured editor viewport FOV to the default camera
@@ -2530,6 +2520,11 @@ bool EditorViewportSettings::StickySelectEnabled() const
return SandboxEditor::StickySelectEnabled();
}
AZ::Vector3 EditorViewportSettings::DefaultEditorCameraPosition() const
{
return SandboxEditor::DefaultEditorCameraPosition();
}
AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once);
bool EditorViewportWidget::ShouldPreviewFullscreen() const
@@ -2641,5 +2636,4 @@ void EditorViewportWidget::StopFullscreenPreview()
// Show the main window
MainWindow::instance()->show();
}
#include <moc_EditorViewportWidget.cpp>
+1
View File
@@ -78,6 +78,7 @@ struct EditorViewportSettings : public AzToolsFramework::ViewportInteraction::Vi
float ManipulatorLineBoundWidth() const override;
float ManipulatorCircleBoundWidth() const override;
bool StickySelectEnabled() const override;
AZ::Vector3 DefaultEditorCameraPosition() const override;
};
// EditorViewportWidget window
+14 -14
View File
@@ -31,11 +31,11 @@
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
//////////////////////////////////////////////////////////////////////////
#define MUSIC_LEVEL_LIBRARY_FILE "Music.xml"
#define MATERIAL_LEVEL_LIBRARY_FILE "Materials.xml"
#define RESOURCE_LIST_FILE "ResourceList.txt"
#define USED_RESOURCE_LIST_FILE "UsedResourceList.txt"
#define SHADER_LIST_FILE "ShadersList.txt"
#define MUSIC_LEVEL_LIBRARY_FILE "music.xml"
#define MATERIAL_LEVEL_LIBRARY_FILE "materials.xml"
#define RESOURCE_LIST_FILE "resourcelist.txt"
#define USED_RESOURCE_LIST_FILE "usedresourcelist.txt"
#define SHADER_LIST_FILE "shaderslist.txt"
#define GetAValue(rgb) ((BYTE)((rgb) >> 24))
@@ -185,9 +185,9 @@ bool CGameExporter::Export(unsigned int flags, [[maybe_unused]] EEndian eExportE
ExportOcclusionMesh(sLevelPath.toUtf8().data());
//! Export Level data.
CLogFile::WriteLine("Exporting LevelData.xml");
CLogFile::WriteLine("Exporting leveldata.xml");
ExportLevelData(sLevelPath);
CLogFile::WriteLine("Exporting LevelData.xml done.");
CLogFile::WriteLine("Exporting leveldata.xml done.");
ExportLevelInfo(sLevelPath);
@@ -266,26 +266,26 @@ void CGameExporter::ExportOcclusionMesh(const char* pszGamePath)
void CGameExporter::ExportLevelData(const QString& path, bool /*bExportMission*/)
{
IEditor* pEditor = GetIEditor();
pEditor->SetStatusText(QObject::tr("Exporting LevelData.xml..."));
pEditor->SetStatusText(QObject::tr("Exporting leveldata.xml..."));
char versionString[256];
pEditor->GetFileVersion().ToString(versionString);
XmlNodeRef root = XmlHelpers::CreateXmlNode("LevelData");
XmlNodeRef root = XmlHelpers::CreateXmlNode("leveldata");
root->setAttr("SandboxVersion", versionString);
XmlNodeRef rootAction = XmlHelpers::CreateXmlNode("LevelDataAction");
XmlNodeRef rootAction = XmlHelpers::CreateXmlNode("leveldataaction");
rootAction->setAttr("SandboxVersion", versionString);
//////////////////////////////////////////////////////////////////////////
// Save Level Data XML
//////////////////////////////////////////////////////////////////////////
QString levelDataFile = path + "LevelData.xml";
QString levelDataFile = path + "leveldata.xml";
XmlString xmlData = root->getXML();
CCryMemFile file;
file.Write(xmlData.c_str(), static_cast<unsigned int>(xmlData.length()));
m_levelPak.m_pakFile.UpdateFile(levelDataFile.toUtf8().data(), file);
QString levelDataActionFile = path + "LevelDataAction.xml";
QString levelDataActionFile = path + "leveldataaction.xml";
XmlString xmlDataAction = rootAction->getXML();
CCryMemFile fileAction;
fileAction.Write(xmlDataAction.c_str(), static_cast<unsigned int>(xmlDataAction.length()));
@@ -298,7 +298,7 @@ void CGameExporter::ExportLevelData(const QString& path, bool /*bExportMission*/
if (savedEntities)
{
QString entitiesFile;
entitiesFile = QStringLiteral("%1%2.entities_xml").arg(path, "Mission0");
entitiesFile = QStringLiteral("%1%2.entities_xml").arg(path, "mission0");
m_levelPak.m_pakFile.UpdateFile(entitiesFile.toUtf8().data(), entitySaveBuffer.begin(), static_cast<int>(entitySaveBuffer.size()));
}
}
@@ -326,7 +326,7 @@ void CGameExporter::ExportLevelInfo(const QString& path)
//////////////////////////////////////////////////////////////////////////
// Save LevelInfo file.
//////////////////////////////////////////////////////////////////////////
QString filename = path + "LevelInfo.xml";
QString filename = path + "levelinfo.xml";
XmlString xmlData = root->getXML();
CCryMemFile file;
@@ -38,6 +38,8 @@ namespace UnitTest
void BeginCursorCapture() override;
void EndCursorCapture() override;
bool IsMouseOver() const override;
void SetOverrideCursor(AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride) override;
void ClearOverrideCursor() override;
private:
AzToolsFramework::QtEventToAzInputMapper* m_inputChannelMapper = nullptr;
@@ -58,6 +60,17 @@ namespace UnitTest
return true;
}
void ViewportMouseCursorRequestImpl::SetOverrideCursor(
[[maybe_unused]] AzToolsFramework::ViewportInteraction::CursorStyleOverride cursorStyleOverride)
{
// noop
}
void ViewportMouseCursorRequestImpl::ClearOverrideCursor()
{
// noop
}
class ModularViewportCameraControllerFixture : public AllocatorsTestFixture
{
public:
@@ -76,6 +76,7 @@ namespace AZ
else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_ProviderIsDown)
{
message = "Failed to put entries/dependencies into source control as the provider is not available.\n";
reportAsWarning = true;
}
else if (info.m_status == AzToolsFramework::SourceControlStatus::SCS_CertificateInvalid)
{
@@ -192,11 +192,86 @@ namespace AZ
};
//! SettingsRegistry notifier handler which is responsible for loading
//! the project.json file at the new project path
//! if an update to '<BootstrapSettingsRootKey>/project_path' key occurs.
struct ProjectPathChangedEventHandler
{
ProjectPathChangedEventHandler(AZ::SettingsRegistryInterface& registry)
: m_registry{ registry }
{
}
void operator()(AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
// Update the project settings when the project path is set
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
const auto projectPathKey = FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
AZ::IO::FixedMaxPath newProjectPath;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectPathKey, path)
&& m_registry.Get(newProjectPath.Native(), projectPathKey) && newProjectPath != m_oldProjectPath)
{
// Update old Project path before attempting to merge in new Settings Registry values in order to prevent recursive calls
m_oldProjectPath = newProjectPath;
// Merge the project.json file into settings registry under ProjectSettingsRootKey path.
AZ::IO::FixedMaxPath projectMetadataFile{ AZ::SettingsRegistryMergeUtils::FindEngineRoot(m_registry) / newProjectPath };
projectMetadataFile /= "project.json";
m_registry.MergeSettingsFile(projectMetadataFile.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
// Update all the runtime file paths based on the new "project_path" value.
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(m_registry);
}
}
private:
AZ::IO::FixedMaxPath m_oldProjectPath;
AZ::SettingsRegistryInterface& m_registry;
};
//! SettingsRegistry notifier handler which adds the project name as a specialization tag
//! to the registry
//! if an update to '<ProjectSettingsRootKey>/project_name' key occurs.
struct ProjectNameChangedEventHandler
{
ProjectNameChangedEventHandler(AZ::SettingsRegistryInterface& registry)
: m_registry{ registry }
{
}
void operator()(AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
// Update the project specialization when the project name is set
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
const auto projectNameKey = FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey) + "/project_name";
FixedValueString newProjectName;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectNameKey, path)
&& m_registry.Get(newProjectName, projectNameKey) && newProjectName != m_oldProjectName)
{
// Add the project_name as a specialization for loading the build system dependency .setreg files
auto newProjectNameSpecialization = FixedValueString::format("%s/%.*s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
aznumeric_cast<int>(newProjectName.size()), newProjectName.data());
auto oldProjectNameSpecialization = FixedValueString::format("%s/%s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
m_oldProjectName.c_str());
m_registry.Remove(oldProjectNameSpecialization);
m_oldProjectName = newProjectName;
m_registry.Set(newProjectNameSpecialization, true);
}
}
private:
AZ::SettingsRegistryInterface::FixedValueString m_oldProjectName;
AZ::SettingsRegistryInterface& m_registry;
};
//! SettingsRegistry notifier handler which updates relevant registry settings based
//! on an update to '/Amazon/AzCore/Bootstrap/project_path' key.
struct UpdateProjectSettingsEventHandler
struct UpdateCommandLineEventHandler
{
UpdateProjectSettingsEventHandler(AZ::SettingsRegistryInterface& registry, AZ::CommandLine& commandLine)
UpdateCommandLineEventHandler(AZ::SettingsRegistryInterface& registry, AZ::CommandLine& commandLine)
: m_registry{ registry }
, m_commandLine{ commandLine }
{
@@ -204,70 +279,14 @@ namespace AZ
void operator()(AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
// #1 Update the project settings when the project path is set
const auto projectPathKey = FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path";
AZ::IO::FixedMaxPath newProjectPath;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectPathKey, path)
&& m_registry.Get(newProjectPath.Native(), projectPathKey) && newProjectPath != m_oldProjectPath)
{
UpdateProjectSettingsFromProjectPath(AZ::IO::PathView(newProjectPath));
}
// #2 Update the project specialization when the project name is set
const auto projectNameKey = FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey) + "/project_name";
FixedValueString newProjectName;
if (SettingsRegistryMergeUtils::IsPathAncestorDescendantOrEqual(projectNameKey, path)
&& m_registry.Get(newProjectName, projectNameKey) && newProjectName != m_oldProjectName)
{
UpdateProjectSpecializationFromProjectName(newProjectName);
}
// #3 Update the ComponentApplication CommandLine instance when the command line settings are merged into the Settings Registry
// Update the ComponentApplication CommandLine instance when the command line settings are merged into the Settings Registry
if (path == AZ::SettingsRegistryMergeUtils::CommandLineValueChangedKey)
{
UpdateCommandLine();
AZ::SettingsRegistryMergeUtils::GetCommandLineFromRegistry(m_registry, m_commandLine);
}
}
//! Add the project name as a specialization underneath the /Amazon/AzCore/Settings/Specializations path
//! and remove the current project name specialization if one exists.
void UpdateProjectSpecializationFromProjectName(AZStd::string_view newProjectName)
{
using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString;
// Add the project_name as a specialization for loading the build system dependency .setreg files
auto newProjectNameSpecialization = FixedValueString::format("%s/%.*s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
aznumeric_cast<int>(newProjectName.size()), newProjectName.data());
auto oldProjectNameSpecialization = FixedValueString::format("%s/%s", AZ::SettingsRegistryMergeUtils::SpecializationsRootKey,
m_oldProjectName.c_str());
m_registry.Remove(oldProjectNameSpecialization);
m_oldProjectName = newProjectName;
m_registry.Set(newProjectNameSpecialization, true);
}
void UpdateProjectSettingsFromProjectPath(AZ::IO::PathView newProjectPath)
{
// Update old Project path before attempting to merge in new Settings Registry values in order to prevent recursive calls
m_oldProjectPath = newProjectPath;
// Merge the project.json file into settings registry under ProjectSettingsRootKey path.
AZ::IO::FixedMaxPath projectMetadataFile{ AZ::SettingsRegistryMergeUtils::FindEngineRoot(m_registry) / newProjectPath };
projectMetadataFile /= "project.json";
m_registry.MergeSettingsFile(projectMetadataFile.Native(),
AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey);
// Update all the runtime file paths based on the new "project_path" value.
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(m_registry);
}
void UpdateCommandLine()
{
AZ::SettingsRegistryMergeUtils::GetCommandLineFromRegistry(m_registry, m_commandLine);
}
private:
AZ::IO::FixedMaxPath m_oldProjectPath;
AZ::SettingsRegistryInterface::FixedValueString m_oldProjectName;
AZ::SettingsRegistryInterface& m_registry;
AZ::CommandLine& m_commandLine;
};
@@ -462,7 +481,12 @@ namespace AZ
// 1. The 'project_path' key changes
// 2. The project specialization when the 'project-name' key changes
// 3. The ComponentApplication command line when the command line is stored to the registry
m_projectChangedHandler = m_settingsRegistry->RegisterNotifier(UpdateProjectSettingsEventHandler{ *m_settingsRegistry, m_commandLine });
m_projectPathChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectPathChangedEventHandler{
*m_settingsRegistry });
m_projectNameChangedHandler = m_settingsRegistry->RegisterNotifier(ProjectNameChangedEventHandler{
*m_settingsRegistry });
m_commandLineUpdatedHandler = m_settingsRegistry->RegisterNotifier(UpdateCommandLineEventHandler{
*m_settingsRegistry, m_commandLine });
// Merge Command Line arguments
constexpr bool executeRegDumpCommands = false;
@@ -515,11 +539,12 @@ namespace AZ
Destroy();
}
// The m_projectChangedHandler stores an AZStd::function internally
// which allocates using the AZ SystemAllocator
// m_projectChangedHandler is being default value initialized
// to clear out the AZStd::function
m_projectChangedHandler = {};
// The SettingsRegistry Notify handlers stores an AZStd::function internally
// which may allocates using the AZ SystemAllocator(if the functor > 16 bytes)
// The handlers are being default value initialized to clear out the AZStd::function
m_commandLineUpdatedHandler = {};
m_projectNameChangedHandler = {};
m_projectPathChangedHandler = {};
// Delete the AZ::IConsole if it was created by this application instance
if (m_ownsConsole)
@@ -390,7 +390,9 @@ namespace AZ
AZ::IO::FixedMaxPath m_engineRoot;
AZ::IO::FixedMaxPath m_appRoot;
AZ::SettingsRegistryInterface::NotifyEventHandler m_projectChangedHandler;
AZ::SettingsRegistryInterface::NotifyEventHandler m_projectPathChangedHandler;
AZ::SettingsRegistryInterface::NotifyEventHandler m_projectNameChangedHandler;
AZ::SettingsRegistryInterface::NotifyEventHandler m_commandLineUpdatedHandler;
// ConsoleFunctorHandle is responsible for unregistering the Settings Registry Console
// from the m_console member when it goes out of scope
@@ -639,9 +639,10 @@ namespace AZ
{
// Make sure the there is a JSON object at the ConsoleRuntimeCommandKey or ConsoleAutoexecKey
// So that JSON Patch is able to add values underneath that object (JSON Patch doesn't create intermediate objects)
settingsRegistry.MergeSettings(R"({ "Amazon": { "AzCore": { "Runtime": { "ConsoleCommands": {} } } })"
R"(,"O3DE": { "Autoexec": { "ConsoleCommands": {} } } })",
SettingsRegistryInterface::Format::JsonMergePatch);
settingsRegistry.MergeSettings(R"({})", SettingsRegistryInterface::Format::JsonMergePatch,
IConsole::ConsoleRuntimeCommandKey);
settingsRegistry.MergeSettings(R"({})", SettingsRegistryInterface::Format::JsonMergePatch,
IConsole::ConsoleAutoexecCommandKey);
m_consoleCommandKeyHandler = settingsRegistry.RegisterNotifier(ConsoleCommandKeyNotificationHandler{ settingsRegistry, *this });
JsonApplyPatchSettings applyPatchSettings;
+3 -1
View File
@@ -577,7 +577,9 @@ namespace AZ
}
azstrcat(lines[i], AZ_ARRAY_SIZE(lines[i]), "\n");
AZ_Printf(window, "%s", lines[i]); // feed back into the trace system so that listeners can get it.
// Use Output instead of AZ_Printf to be consistent with the exception output code and avoid
// this accidentally being suppressed as a normal message
Output(window, lines[i]);
}
}
}
@@ -0,0 +1,254 @@
/*
* 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 <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
namespace AZ
{
JsonSerializationResult::ResultCode JsonImportResolver::ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element)
{
using namespace JsonSerializationResult;
for (auto& path : importPathStack)
{
if (importPath == path)
{
return settings.m_reporting(
AZStd::string::format("'%s' was already imported in this chain. This indicates a cyclic dependency.", importPath.c_str()),
ResultCode(Tasks::Import, Outcomes::Catastrophic), element);
}
}
importPathStack.push_back(importPath);
AZ::StackedString importElement(AZ::StackedString::Format::JsonPointer);
JsonImportSettings nestedImportSettings;
nestedImportSettings.m_importer = settings.m_importer;
nestedImportSettings.m_reporting = settings.m_reporting;
nestedImportSettings.m_resolveFlags = ImportTracking::Dependencies;
ResultCode result = ResolveImports(jsonDoc, allocator, importPathStack, nestedImportSettings, importElement);
importPathStack.pop_back();
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject())
{
for (auto& field : jsonDoc.GetObject())
{
if(strncmp(field.name.GetString(), JsonSerialization::ImportDirectiveIdentifier, field.name.GetStringLength()) == 0)
{
const rapidjson::Value& importDirective = field.value;
AZ::IO::FixedMaxPath importAbsPath = importPathStack.back();
importAbsPath.RemoveFilename();
AZStd::string importName;
if (importDirective.IsObject())
{
auto filenameField = importDirective.FindMember("filename");
if (filenameField != importDirective.MemberEnd())
{
importName = AZStd::string(filenameField->value.GetString(), filenameField->value.GetStringLength());
}
}
else
{
importName = AZStd::string(importDirective.GetString(), importDirective.GetStringLength());
}
importAbsPath.Append(importName);
rapidjson::Value patch;
ResultCode resolveResult = settings.m_importer->ResolveImport(&jsonDoc, patch, importDirective, importAbsPath, allocator);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
if ((settings.m_resolveFlags & ImportTracking::Imports) == ImportTracking::Imports)
{
rapidjson::Pointer path(element.Get().data(), element.Get().size());
settings.m_importer->AddImportDirective(path, importName);
}
if ((settings.m_resolveFlags & ImportTracking::Dependencies) == ImportTracking::Dependencies)
{
settings.m_importer->AddImportedFile(importAbsPath.String());
}
ResultCode result = ResolveNestedImports(jsonDoc, allocator, importPathStack, settings, importAbsPath, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
settings.m_importer->ApplyPatch(jsonDoc, patch, allocator);
}
else if (field.value.IsObject() || field.value.IsArray())
{
ScopedStackedString entryName(element, AZStd::string_view(field.name.GetString(), field.name.GetStringLength()));
ResultCode result = ResolveImports(field.value, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
}
else if(jsonDoc.IsArray())
{
int index = 0;
for (rapidjson::Value::ValueIterator elem = jsonDoc.Begin(); elem != jsonDoc.End(); ++elem, ++index)
{
if (!elem->IsObject() && !elem->IsArray())
{
continue;
}
ScopedStackedString entryName(element, index);
ResultCode result = ResolveImports(*elem, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject() || jsonDoc.IsArray())
{
const BaseJsonImporter::ImportDirectivesList& importDirectives = settings.m_importer->GetImportDirectives();
for (auto& import : importDirectives)
{
rapidjson::Pointer importPtr = import.first;
rapidjson::Value* currentValue = importPtr.Get(jsonDoc);
rapidjson::Value importedValue(rapidjson::kObjectType);
importedValue.AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(import.second.c_str()), allocator);
ResultCode resolveResult = JsonSerialization::ResolveImports(importedValue, allocator, settings);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
rapidjson::Value patch;
settings.m_importer->CreatePatch(patch, importedValue, *currentValue, allocator);
settings.m_importer->RestoreImport(currentValue, patch, allocator, import.second);
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
auto importedObject = JsonSerializationUtils::ReadJsonFile(importedFilePath.Native());
if (importedObject.IsSuccess())
{
rapidjson::Value& importedDoc = importedObject.GetValue();
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
}
else
{
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, const AZStd::string& importFilename)
{
using namespace JsonSerializationResult;
importPtr->SetObject();
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
rapidjson::Value importDirective(rapidjson::kObjectType);
importDirective.AddMember(rapidjson::StringRef("filename"), rapidjson::StringRef(importFilename.c_str()), allocator);
importDirective.AddMember(rapidjson::StringRef("patch"), patch, allocator);
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), importDirective, allocator);
}
else
{
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(importFilename.c_str()), allocator);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
return AZ::JsonSerialization::ApplyPatch(target, allocator, patch, JsonMergeApproach::JsonMergePatch);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator)
{
return JsonSerialization::CreatePatch(patch, allocator, source, target, JsonMergeApproach::JsonMergePatch);
}
void BaseJsonImporter::AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile)
{
m_importDirectives.emplace_back(jsonPtr, AZStd::move(importFile));
}
void BaseJsonImporter::AddImportedFile(AZStd::string importedFile)
{
m_importedFiles.insert(AZStd::move(importedFile));
}
const BaseJsonImporter::ImportDirectivesList& BaseJsonImporter::GetImportDirectives()
{
return m_importDirectives;
}
const BaseJsonImporter::ImportedFilesList& BaseJsonImporter::GetImportedFiles()
{
return m_importedFiles;
}
} // namespace AZ
@@ -0,0 +1,108 @@
/*
* 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
*
*/
#pragma once
#include<AzCore/IO/Path/Path.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/pointer.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/Serialization/Json/JsonSerializationResult.h>
#include <AzCore/Serialization/Json/StackedString.h>
namespace AZ
{
struct JsonImportSettings;
class BaseJsonImporter
{
public:
AZ_RTTI(BaseJsonImporter, "{7B225807-7B43-430F-8B11-C794DCF5ACA5}");
using ImportDirectivesList = AZStd::vector<AZStd::pair<rapidjson::Pointer, AZStd::string>>;
using ImportedFilesList = AZStd::unordered_set<AZStd::string>;
virtual JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator,
const AZStd::string& importFilename);
virtual JsonSerializationResult::ResultCode ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator);
void AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile);
const ImportDirectivesList& GetImportDirectives();
void AddImportedFile(AZStd::string importedFile);
const ImportedFilesList& GetImportedFiles();
virtual ~BaseJsonImporter() = default;
protected:
ImportDirectivesList m_importDirectives;
ImportedFilesList m_importedFiles;
};
enum class ImportTracking : AZ::u8
{
None = 0,
Dependencies = (1<<0),
Imports = (1<<1),
All = (Dependencies | Imports)
};
AZ_DEFINE_ENUM_BITWISE_OPERATORS(ImportTracking);
class JsonImportResolver final
{
public:
using ImportPathStack = AZStd::vector<AZ::IO::FixedMaxPath>;
JsonImportResolver() = delete;
JsonImportResolver& operator=(const JsonImportResolver& rhs) = delete;
JsonImportResolver& operator=(JsonImportResolver&& rhs) = delete;
JsonImportResolver(const JsonImportResolver& rhs) = delete;
JsonImportResolver(JsonImportResolver&& rhs) = delete;
~JsonImportResolver() = delete;
static JsonSerializationResult::ResultCode ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element);
static JsonSerializationResult::ResultCode RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
static JsonSerializationResult::ResultCode ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element);
};
struct JsonImportSettings final
{
JsonSerializationResult::JsonIssueCallback m_reporting;
BaseJsonImporter* m_importer = nullptr;
ImportTracking m_resolveFlags = ImportTracking::All;
AZ::IO::FixedMaxPath m_loadedJsonPath;
};
} // namespace AZ
@@ -706,7 +706,7 @@ namespace AZ
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
}
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -717,6 +717,10 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
// Do an extra pass to find all the fields that are removed.
@@ -751,7 +755,7 @@ namespace AZ
rapidjson::Value value;
ResultCode result = CreateMergePatchInternal(value, allocator,
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -762,11 +766,20 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
if (target.MemberCount() == 0)
{
resultCode.Combine(settings.m_reporting("Added empty object to JSON Merge Patch.",
ResultCode(Tasks::CreatePatch, Outcomes::Success), element));
}
}
patch = AZStd::move(resultValue);
resultCode.Combine(ResultCode(Tasks::CreatePatch, Outcomes::Success));
return resultCode;
}
else
@@ -10,6 +10,7 @@
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/Json/BaseJsonSerializer.h>
#include <AzCore/Serialization/Json/JsonDeserializer.h>
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonMerger.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/JsonSerializer.h>
@@ -19,11 +20,6 @@
namespace AZ
{
const char* JsonSerialization::TypeIdFieldIdentifier = "$type";
const char* JsonSerialization::DefaultStringIdentifier = "{}";
const char* JsonSerialization::KeyFieldIdentifier = "Key";
const char* JsonSerialization::ValueFieldIdentifier = "Value";
namespace JsonSerializationInternal
{
template<typename T>
@@ -394,6 +390,60 @@ namespace AZ
}
}
JsonSerializationResult::ResultCode JsonSerialization::ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
JsonImportResolver::ImportPathStack importPathStack;
importPathStack.push_back(settings.m_loadedJsonPath);
StackedString element(StackedString::Format::JsonPointer);
return JsonImportResolver::ResolveImports(jsonDoc, allocator, importPathStack, settings, element);
}
JsonSerializationResult::ResultCode JsonSerialization::RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
settings.m_resolveFlags = ImportTracking::None;
return JsonImportResolver::RestoreImports(jsonDoc, allocator, settings);
}
JsonSerializationResult::ResultCode JsonSerialization::DefaultIssueReporter(AZStd::string& scratchBuffer,
AZStd::string_view message, JsonSerializationResult::ResultCode result, AZStd::string_view path)
{
@@ -18,6 +18,8 @@
namespace AZ
{
class BaseJsonSerializer;
struct JsonImportSettings;
enum class JsonMergeApproach
{
@@ -51,10 +53,11 @@ namespace AZ
class JsonSerialization final
{
public:
static const char* TypeIdFieldIdentifier;
static const char* DefaultStringIdentifier;
static const char* KeyFieldIdentifier;
static const char* ValueFieldIdentifier;
static constexpr const char* TypeIdFieldIdentifier = "$type";
static constexpr const char* DefaultStringIdentifier = "{}";
static constexpr const char* KeyFieldIdentifier = "Key";
static constexpr const char* ValueFieldIdentifier = "Value";
static constexpr const char* ImportDirectiveIdentifier = "$import";
//! Merges two json values together by applying "patch" to "target" using the selected merge algorithm.
//! This version of ApplyPatch is destructive to "target". If the patch can't be correctly applied it will
@@ -284,6 +287,22 @@ namespace AZ
//! @return An enum containing less, equal or greater. In case of an error, the value for the enum will "error".
static JsonSerializerCompareResult Compare(const rapidjson::Value& lhs, const rapidjson::Value& rhs);
//! Resolves all import directives, including nested imports, in the given document. An importer object needs to be passed
//! in through the settings.
//! @param jsonDoc The json document in which to resolve imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are resolved.
static JsonSerializationResult::ResultCode ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
//! Restores all import directives that were present in the json document. The same importer object that was
//! passed into ResolveImports through the settings needs to be passed here through settings as well.
//! @param jsonDoc The json document in which to restore imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are restored.
static JsonSerializationResult::ResultCode RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
JsonSerialization() = delete;
~JsonSerialization() = delete;
@@ -69,6 +69,9 @@ namespace AZ
case Tasks::CreatePatch:
target.append("a create patch operation ");
break;
case Tasks::Import:
target.append("an import operation");
break;
default:
target.append("an unknown operation ");
break;
@@ -32,7 +32,8 @@ namespace AZ
ReadField, //!< Task to read a field from JSON to a value.
WriteValue, //!< Task to write a value to a JSON field.
Merge, //!< Task to merge two JSON values/documents together.
CreatePatch //!< Task to create a patch to transform one value/document to another.
CreatePatch, //!< Task to create a patch to transform one value/document to another.
Import //!< Task to import a JSON document.
};
//! Describes how the task was processed.
@@ -192,26 +192,34 @@ namespace AZ
//! @param path An offset at which traversal should start.
//! @return Whether or not entries could be visited.
virtual bool Visit(const VisitorCallback& callback, AZStd::string_view path) const = 0;
//! Register a callback that will be called whenever an entry gets a new/updated value.
//!
//! @callback The function to call when an entry gets a new/updated value.
[[nodiscard]] virtual NotifyEventHandler RegisterNotifier(const NotifyCallback& callback) = 0;
//! Register a callback that will be called whenever an entry gets a new/updated value.
//! @callback The function to call when an entry gets a new/updated value.
[[nodiscard]] virtual NotifyEventHandler RegisterNotifier(NotifyCallback&& callback) = 0;
//! @return NotifyEventHandler instance which must persist to receive event signal
[[nodiscard]] virtual NotifyEventHandler RegisterNotifier(NotifyCallback callback) = 0;
//! Register a notify event handler with the NotifyEvent.
//! The handler will be called whenever an entry gets a new/updated value.
//! @param handler The handler to register with the NotifyEvent.
virtual void RegisterNotifier(NotifyEventHandler& handler) = 0;
//! Register a function that will be called before a file is merged.
//! @callback The function to call before a file is merged.
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(const PreMergeEventCallback& callback) = 0;
//! Register a function that will be called before a file is merged.
//! @callback The function to call before a file is merged.
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback&& callback) = 0;
//! @param callback The function to call before a file is merged.
//! @return PreMergeEventHandler instance which must persist to receive event signal
[[nodiscard]] virtual PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback callback) = 0;
//! Register a pre-merge handler with the PreMergeEvent.
//! The handler will be called before a file is merged.
//! @param handler The hanlder to register with the PreMergeEvent.
virtual void RegisterPreMergeEvent(PreMergeEventHandler& handler) = 0;
//! Register a function that will be called after a file is merged.
//! @callback The function to call after a file is merged.
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(const PostMergeEventCallback& callback) = 0;
//! Register a function that will be called after a file is merged.
//! @callback The function to call after a file is merged.
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback&& callback) = 0;
//! @param callback The function to call after a file is merged.
//! @return PostMergeEventHandler instance which must persist to receive event signal
[[nodiscard]] virtual PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback callback) = 0;
//! Register a post-merge hahndler with the PostMergeEvent.
//! The handler will be called after a file is merged.
//! @param handler The handler to register with the PostmergeEVent.
virtual void RegisterPostMergeEvent(PostMergeEventHandler& hanlder) = 0;
//! Gets the boolean value at the provided path.
//! @param result The target to write the result to.
@@ -326,23 +334,25 @@ namespace AZ
//! - all digits and dot -> floating point number
//! - Everything else is considered a string.
//! @param argument The command line argument.
//! @param structure which contains functors which determine what characters are delimiters
//! @param anchorKey The key where the merged command line argument will be anchored under
//! @param commandLineSettings structure which contains functors which determine what characters are delimiters
//! @return True if the command line argument could be parsed, otherwise false.
virtual bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view rootKey = "",
virtual bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view anchorKey = "",
const CommandLineArgumentSettings& commandLineSettings = {}) = 0;
//! Merges the json data provided into the settings registry.
//! @param data The json data stored in a string.
//! @param format The format of the provided data.
//! @param anchorKey The key where the merged json content will be anchored under.
//! @return True if the data was successfully merged, otherwise false.
virtual bool MergeSettings(AZStd::string_view data, Format format) = 0;
virtual bool MergeSettings(AZStd::string_view data, Format format, AZStd::string_view anchorKey = "") = 0;
//! Loads a settings file and merges it into the registry.
//! @param path The path to the registry file.
//! @param format The format of the text data in the file at the provided path.
//! @param rootKey The key where the root of the settings file will be stored under.
//! @param anchorKey The key where the content of the settings file will be anchored.
//! @param scratchBuffer An optional buffer that's used to load the file into. Use this when loading multiple patches to
//! reduce the number of intermediate memory allocations.
//! @return True if the registry file was successfully merged, otherwise false.
virtual bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view rootKey = "",
virtual bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view anchorKey = "",
AZStd::vector<char>* scratchBuffer = nullptr) = 0;
//! Loads all settings files in a folder and merges them into the registry.
//! With the specializations "a" and "b" and platform "c" the files would be loaded in the order:
@@ -357,11 +367,12 @@ namespace AZ
//! @param platform An optional name of a platform. Platform overloads are located at <path>/Platform/<platform>/
//! Files in a platform are applied in the same order as for the main folder but always after the same file
//! in the main folder.
//! @param anchorKey The registry path location where the settings will be anchored
//! @param scratchBuffer An optional buffer that's used to load the file into. Use this when loading multiple patches to
//! reduce the number of intermediate memory allocations.
//! @return True if the registry folder was successfully merged, otherwise false.
virtual bool MergeSettingsFolder(AZStd::string_view path, const Specializations& specializations,
AZStd::string_view platform = {}, AZStd::string_view rootKey = "", AZStd::vector<char>* scratchBuffer = nullptr) = 0;
AZStd::string_view platform = {}, AZStd::string_view anchorKey = "", AZStd::vector<char>* scratchBuffer = nullptr) = 0;
//! Stores the settings structure which is used when merging settings to the Settings Registry
//! using JSON Merge Patch or JSON Merge Patch.
@@ -13,7 +13,7 @@
#include <AzCore/IO/FileReader.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/JSON/error/en.h>
#include <AzCore/NativeUI//NativeUIRequests.h>
#include <AzCore/NativeUI/NativeUIRequests.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/StackedString.h>
#include <AzCore/Settings/SettingsRegistryImpl.h>
@@ -21,6 +21,34 @@
#include <AzCore/std/sort.h>
#include <AzCore/std/parallel/scoped_lock.h>
namespace AZ::SettingsRegistryImplInternal
{
AZ::SettingsRegistryInterface::Type RapidjsonToSettingsRegistryType(const rapidjson::Value& value)
{
using Type = AZ::SettingsRegistryInterface::Type;
switch (value.GetType())
{
case rapidjson::Type::kNullType:
return Type::Null;
case rapidjson::Type::kFalseType:
return Type::Boolean;
case rapidjson::Type::kTrueType:
return Type::Boolean;
case rapidjson::Type::kObjectType:
return Type::Object;
case rapidjson::Type::kArrayType:
return Type::Array;
case rapidjson::Type::kStringType:
return Type::String;
case rapidjson::Type::kNumberType:
return value.IsDouble() ? Type::FloatingPoint :
Type::Integer;
}
return Type::NoType;
}
}
namespace AZ
{
template<typename T>
@@ -28,7 +56,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -70,7 +98,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -161,7 +189,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -212,17 +240,7 @@ namespace AZ
return Visit(visitor, path);
}
auto SettingsRegistryImpl::RegisterNotifier(const NotifyCallback& callback) -> NotifyEventHandler
{
NotifyEventHandler notifyHandler{ callback };
{
AZStd::scoped_lock lock(m_notifierMutex);
notifyHandler.Connect(m_notifiers);
}
return notifyHandler;
}
auto SettingsRegistryImpl::RegisterNotifier(NotifyCallback&& callback) -> NotifyEventHandler
auto SettingsRegistryImpl::RegisterNotifier(NotifyCallback callback) -> NotifyEventHandler
{
NotifyEventHandler notifyHandler{ AZStd::move(callback) };
{
@@ -232,23 +250,19 @@ namespace AZ
return notifyHandler;
}
auto SettingsRegistryImpl::RegisterNotifier(NotifyEventHandler& notifyHandler) -> void
{
AZStd::scoped_lock lock(m_notifierMutex);
notifyHandler.Connect(m_notifiers);
}
void SettingsRegistryImpl::ClearNotifiers()
{
AZStd::scoped_lock lock(m_notifierMutex);
m_notifiers.DisconnectAllHandlers();
}
auto SettingsRegistryImpl::RegisterPreMergeEvent(const PreMergeEventCallback& callback) -> PreMergeEventHandler
{
PreMergeEventHandler preMergeHandler{ callback };
{
AZStd::scoped_lock lock(m_settingMutex);
preMergeHandler.Connect(m_preMergeEvent);
}
return preMergeHandler;
}
auto SettingsRegistryImpl::RegisterPreMergeEvent(PreMergeEventCallback&& callback) -> PreMergeEventHandler
auto SettingsRegistryImpl::RegisterPreMergeEvent(PreMergeEventCallback callback) -> PreMergeEventHandler
{
PreMergeEventHandler preMergeHandler{ AZStd::move(callback) };
{
@@ -258,17 +272,13 @@ namespace AZ
return preMergeHandler;
}
auto SettingsRegistryImpl::RegisterPostMergeEvent(const PostMergeEventCallback& callback) -> PostMergeEventHandler
auto SettingsRegistryImpl::RegisterPreMergeEvent(PreMergeEventHandler& preMergeHandler) -> void
{
PostMergeEventHandler postMergeHandler{ callback };
{
AZStd::scoped_lock lock(m_settingMutex);
postMergeHandler.Connect(m_postMergeEvent);
}
return postMergeHandler;
AZStd::scoped_lock lock(m_settingMutex);
preMergeHandler.Connect(m_preMergeEvent);
}
auto SettingsRegistryImpl::RegisterPostMergeEvent(PostMergeEventCallback&& callback) -> PostMergeEventHandler
auto SettingsRegistryImpl::RegisterPostMergeEvent(PostMergeEventCallback callback) -> PostMergeEventHandler
{
PostMergeEventHandler postMergeHandler{ AZStd::move(callback) };
{
@@ -278,6 +288,12 @@ namespace AZ
return postMergeHandler;
}
auto SettingsRegistryImpl::RegisterPostMergeEvent(PostMergeEventHandler& postMergeHandler) -> void
{
AZStd::scoped_lock lock(m_settingMutex);
postMergeHandler.Connect(m_postMergeEvent);
}
void SettingsRegistryImpl::ClearMergeEvents()
{
AZStd::scoped_lock lock(m_settingMutex);
@@ -297,7 +313,36 @@ namespace AZ
localNotifierEvent = AZStd::move(m_notifiers);
}
localNotifierEvent.Signal(jsonPath, type);
// Signal the NotifyEvent for each queued argument
decltype(m_signalNotifierQueue) localNotifierQueue;
{
AZStd::scoped_lock signalLock(m_signalMutex);
m_signalNotifierQueue.push_back({ FixedValueString{jsonPath}, type });
// If the signal count was 0, then a dispatch is in progress
if (m_signalCount++ == 0)
{
AZStd::swap(localNotifierQueue, m_signalNotifierQueue);
}
}
while (!localNotifierQueue.empty())
{
for (SignalNotifierArgs notifierArgs : localNotifierQueue)
{
localNotifierEvent.Signal(notifierArgs.m_jsonPath, notifierArgs.m_type);
}
// Clear the local notifier queue and check if more notifiers have been added
localNotifierQueue = {};
{
AZStd::scoped_lock signalLock(m_signalMutex);
AZStd::swap(localNotifierQueue, m_signalNotifierQueue);
}
}
{
AZStd::scoped_lock signalLock(m_signalMutex);
--m_signalCount;
}
{
// Swap the local handlers with the current m_notifiers which
@@ -314,39 +359,19 @@ namespace AZ
{
if (path.empty())
{
//rapidjson::Pointer assets that the supplied string
//rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
}
rapidjson::Pointer pointer(path.data(), path.length());
if (pointer.IsValid())
{
AZStd::scoped_lock lock(m_settingMutex);
const rapidjson::Value* value = pointer.Get(m_settings);
if (value)
if (const rapidjson::Value* value = pointer.Get(m_settings); value != nullptr)
{
switch (value->GetType())
{
case rapidjson::Type::kNullType:
return Type::Null;
case rapidjson::Type::kFalseType:
return Type::Boolean;
case rapidjson::Type::kTrueType:
return Type::Boolean;
case rapidjson::Type::kObjectType:
return Type::Object;
case rapidjson::Type::kArrayType:
return Type::Array;
case rapidjson::Type::kStringType:
return Type::String;
case rapidjson::Type::kNumberType:
return
value->IsDouble() ? Type::FloatingPoint :
Type::Integer;
}
return SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(*value);
}
}
return Type::NoType;
@@ -392,7 +417,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -471,13 +496,12 @@ namespace AZ
{
if (path.empty())
{
//rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
}
rapidjson::Pointer pointer(path.data(), path.length());
if (pointer.IsValid())
{
@@ -486,10 +510,14 @@ namespace AZ
value, nullptr, valueTypeID, m_serializationSettings);
if (jsonResult.GetProcessing() != JsonSerializationResult::Processing::Halted)
{
AZStd::scoped_lock lock(m_settingMutex);
rapidjson::Value& setting = pointer.Create(m_settings, m_settings.GetAllocator());
setting = AZStd::move(store);
SignalNotifier(path, Type::Object);
auto anchorType = Type::NoType;
{
AZStd::scoped_lock lock(m_settingMutex);
rapidjson::Value& setting = pointer.Create(m_settings, m_settings.GetAllocator());
setting = AZStd::move(store);
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(setting);
}
SignalNotifier(path, anchorType);
return true;
}
}
@@ -500,7 +528,7 @@ namespace AZ
{
if (path.empty())
{
// rapidjson::Pointer assets that the supplied string
// rapidjson::Pointer asserts that the supplied string
// is not nullptr even if the supplied size is 0
// Setting to empty string to prevent assert
path = "";
@@ -605,7 +633,7 @@ namespace AZ
return Set(key, value);
}
bool SettingsRegistryImpl::MergeSettings(AZStd::string_view data, Format format)
bool SettingsRegistryImpl::MergeSettings(AZStd::string_view data, Format format, AZStd::string_view anchorKey)
{
rapidjson::Document jsonPatch;
constexpr int flags = rapidjson::kParseStopWhenDoneFlag | rapidjson::kParseCommentsFlag | rapidjson::kParseTrailingCommasFlag;
@@ -631,17 +659,43 @@ namespace AZ
return false;
}
AZStd::scoped_lock lock(m_settingMutex);
JsonSerializationResult::ResultCode mergeResult =
JsonSerialization::ApplyPatch(m_settings, m_settings.GetAllocator(), jsonPatch, mergeApproach);
if (mergeResult.GetProcessing() != JsonSerializationResult::Processing::Completed)
rapidjson::Pointer anchorPath;
if (!anchorKey.empty())
{
AZ_Error("Settings Registry", false, "Failed to fully merge data into registry.");
return false;
anchorPath = rapidjson::Pointer(anchorKey.data(), anchorKey.size());
if (!anchorPath.IsValid())
{
rapidjson::Pointer pointer(AZ_SETTINGS_REGISTRY_HISTORY_KEY "/-");
AZ_Error("Settings Registry", false, R"(Anchor path "%.*s" is invalid.)", AZ_STRING_ARG(anchorKey));
AZStd::scoped_lock lock(m_settingMutex);
pointer.Create(m_settings, m_settings.GetAllocator()).SetObject()
.AddMember(rapidjson::StringRef("Error"), rapidjson::StringRef("Invalid anchor key."), m_settings.GetAllocator())
.AddMember(rapidjson::StringRef("Path"),
rapidjson::Value(anchorKey.data(), aznumeric_caster(anchorKey.size()), m_settings.GetAllocator()),
m_settings.GetAllocator());
return false;
}
}
SignalNotifier("", Type::Object);
auto anchorType = AZ::SettingsRegistryInterface::Type::NoType;
{
AZStd::scoped_lock lock(m_settingMutex);
rapidjson::Value& anchorRoot = anchorPath.IsValid() ? anchorPath.Create(m_settings, m_settings.GetAllocator())
: m_settings;
JsonSerializationResult::ResultCode mergeResult =
JsonSerialization::ApplyPatch(anchorRoot, m_settings.GetAllocator(), jsonPatch, mergeApproach);
if (mergeResult.GetProcessing() != JsonSerializationResult::Processing::Completed)
{
AZ_Error("Settings Registry", false, "Failed to fully merge data into registry.");
return false;
}
// The settings have been successfully merged, query the type at the anchor key
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(anchorRoot);
}
SignalNotifier(anchorKey, anchorType);
return true;
}
@@ -1225,10 +1279,12 @@ namespace AZ
ScopedMergeEvent scopedMergeEvent(m_preMergeEvent, m_postMergeEvent, path, rootKey);
JsonSerializationResult::ResultCode mergeResult(JsonSerializationResult::Tasks::Merge);
auto anchorType = Type::NoType;
if (rootKey.empty())
{
AZStd::scoped_lock lock(m_settingMutex);
mergeResult = JsonSerialization::ApplyPatch(m_settings, m_settings.GetAllocator(), jsonPatch, mergeApproach, m_applyPatchSettings);
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(m_settings);
}
else
{
@@ -1238,6 +1294,7 @@ namespace AZ
AZStd::scoped_lock lock(m_settingMutex);
Value& rootValue = root.Create(m_settings, m_settings.GetAllocator());
mergeResult = JsonSerialization::ApplyPatch(rootValue, m_settings.GetAllocator(), jsonPatch, mergeApproach, m_applyPatchSettings);
anchorType = SettingsRegistryImplInternal::RapidjsonToSettingsRegistryType(rootValue);
}
else
{
@@ -1265,7 +1322,7 @@ namespace AZ
pointer.Create(m_settings, m_settings.GetAllocator()).SetString(path, m_settings.GetAllocator());
}
SignalNotifier("", Type::Object);
SignalNotifier(rootKey, anchorType);
return true;
}
@@ -48,14 +48,14 @@ namespace AZ
Type GetType(AZStd::string_view path) const override;
bool Visit(Visitor& visitor, AZStd::string_view path) const override;
bool Visit(const VisitorCallback& callback, AZStd::string_view path) const override;
[[nodiscard]] NotifyEventHandler RegisterNotifier(const NotifyCallback& callback) override;
[[nodiscard]] NotifyEventHandler RegisterNotifier(NotifyCallback&& callback) override;
[[nodiscard]] NotifyEventHandler RegisterNotifier(NotifyCallback callback) override;
void RegisterNotifier(NotifyEventHandler& hanlder) override;
void ClearNotifiers();
[[nodiscard]] PreMergeEventHandler RegisterPreMergeEvent(const PreMergeEventCallback& callback) override;
[[nodiscard]] PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback&& callback) override;
[[nodiscard]] PostMergeEventHandler RegisterPostMergeEvent(const PostMergeEventCallback& callback) override;
[[nodiscard]] PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback&& callback) override;
[[nodiscard]] PreMergeEventHandler RegisterPreMergeEvent(PreMergeEventCallback callback) override;
void RegisterPreMergeEvent(PreMergeEventHandler& handler) override;
[[nodiscard]] PostMergeEventHandler RegisterPostMergeEvent(PostMergeEventCallback callback) override;
void RegisterPostMergeEvent(PostMergeEventHandler& handler) override;
void ClearMergeEvents();
bool Get(bool& result, AZStd::string_view path) const override;
@@ -76,13 +76,13 @@ namespace AZ
bool Remove(AZStd::string_view path) override;
bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view rootKey,
bool MergeCommandLineArgument(AZStd::string_view argument, AZStd::string_view anchorKey,
const CommandLineArgumentSettings& commandLineSettings) override;
bool MergeSettings(AZStd::string_view data, Format format) override;
bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view rootKey,
bool MergeSettings(AZStd::string_view data, Format format, AZStd::string_view anchorKey = "") override;
bool MergeSettingsFile(AZStd::string_view path, Format format, AZStd::string_view anchorKey = "",
AZStd::vector<char>* scratchBuffer = nullptr) override;
bool MergeSettingsFolder(AZStd::string_view path, const Specializations& specializations,
AZStd::string_view platform, AZStd::string_view rootKey = "", AZStd::vector<char>* scratchBuffer = nullptr) override;
AZStd::string_view platform, AZStd::string_view anchorKey = "", AZStd::vector<char>* scratchBuffer = nullptr) override;
void SetApplyPatchSettings(const AZ::JsonApplyPatchSettings& applyPatchSettings) override;
void GetApplyPatchSettings(AZ::JsonApplyPatchSettings& applyPatchSettings) override;
@@ -121,6 +121,19 @@ namespace AZ
PreMergeEvent m_preMergeEvent;
PostMergeEvent m_postMergeEvent;
//! NOTE: During SignalNotifier, the registered notify event handlers are moved to a local NotifyEvent
//! Therefore setting a value within the registry during signaling will queue future SignalNotifer calls
//! These calls will then be invoked after the current signaling has completex
//! This is done to avoid deadlock if another thread attempts to access register a notifier or signal one
mutable AZStd::mutex m_signalMutex;
struct SignalNotifierArgs
{
FixedValueString m_jsonPath;
Type m_type;
};
AZStd::deque<SignalNotifierArgs> m_signalNotifierQueue;
AZStd::atomic_int m_signalCount{};
rapidjson::Document m_settings;
JsonSerializerSettings m_serializationSettings;
JsonDeserializerSettings m_deserializationSettings;
@@ -23,12 +23,12 @@ namespace AZ
MOCK_CONST_METHOD1(GetType, Type(AZStd::string_view));
MOCK_CONST_METHOD2(Visit, bool(Visitor&, AZStd::string_view));
MOCK_CONST_METHOD2(Visit, bool(const VisitorCallback&, AZStd::string_view));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler(const NotifyCallback&));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler(NotifyCallback&&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler(const PreMergeEventCallback&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler(PreMergeEventCallback&&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler(const PostMergeEventCallback&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler(PostMergeEventCallback&&));
MOCK_METHOD1(RegisterNotifier, NotifyEventHandler(NotifyCallback));
MOCK_METHOD1(RegisterNotifier, void(NotifyEventHandler&));
MOCK_METHOD1(RegisterPreMergeEvent, PreMergeEventHandler(PreMergeEventCallback));
MOCK_METHOD1(RegisterPreMergeEvent, void(PreMergeEventHandler&));
MOCK_METHOD1(RegisterPostMergeEvent, PostMergeEventHandler(PostMergeEventCallback));
MOCK_METHOD1(RegisterPostMergeEvent, void(PostMergeEventHandler&));
MOCK_CONST_METHOD2(Get, bool(bool&, AZStd::string_view));
MOCK_CONST_METHOD2(Get, bool(s64&, AZStd::string_view));
@@ -49,7 +49,7 @@ namespace AZ
MOCK_METHOD1(Remove, bool(AZStd::string_view));
MOCK_METHOD3(MergeCommandLineArgument, bool(AZStd::string_view, AZStd::string_view, const CommandLineArgumentSettings&));
MOCK_METHOD2(MergeSettings, bool(AZStd::string_view, Format));
MOCK_METHOD3(MergeSettings, bool(AZStd::string_view, Format, AZStd::string_view));
MOCK_METHOD4(MergeSettingsFile, bool(AZStd::string_view, Format, AZStd::string_view, AZStd::vector<char>*));
MOCK_METHOD5(
MergeSettingsFolder,
@@ -522,6 +522,8 @@ set(FILES
Serialization/Json/IntSerializer.cpp
Serialization/Json/JsonDeserializer.h
Serialization/Json/JsonDeserializer.cpp
Serialization/Json/JsonImporter.cpp
Serialization/Json/JsonImporter.h
Serialization/Json/JsonMerger.h
Serialization/Json/JsonMerger.cpp
Serialization/Json/JsonSerialization.h
@@ -0,0 +1,413 @@
/*
* 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 <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <Tests/Serialization/Json/JsonSerializationTests.h>
namespace JsonSerializationTests
{
class JsonImportingTests;
class JsonImporterCustom
: public AZ::BaseJsonImporter
{
public:
AZ_RTTI(JsonImporterCustom, "{003F5896-71E0-4A50-A14F-08C319B06AD0}");
AZ::JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) override;
JsonImporterCustom(JsonImportingTests* tests)
{
testClass = tests;
}
private:
JsonImportingTests* testClass;
};
class JsonImportingTests
: public BaseJsonSerializerFixture
{
public:
void SetUp() override
{
BaseJsonSerializerFixture::SetUp();
}
void TearDown() override
{
BaseJsonSerializerFixture::TearDown();
}
void GetTestDocument(const AZStd::string& docName, rapidjson::Document& out)
{
const char *objectJson = R"({
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
})";
const char *arrayJson = R"([
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
])";
const char *nestedImportJson = R"({
"desc" : "Nested Import",
"obj" : {"$import" : "object.json"}
})";
const char *nestedImportCycle1Json = R"({
"desc" : "Nested Import Cycle 1",
"obj" : {"$import" : "nested_import_c2.json"}
})";
const char *nestedImportCycle2Json = R"({
"desc" : "Nested Import Cycle 2",
"obj" : {"$import" : "nested_import_c1.json"}
})";
if (docName.compare("object.json") == 0)
{
out.Parse(objectJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("array.json") == 0)
{
out.Parse(arrayJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import.json") == 0)
{
out.Parse(nestedImportJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c1.json") == 0)
{
out.Parse(nestedImportCycle1Json);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c2.json") == 0)
{
out.Parse(nestedImportCycle2Json);
ASSERT_FALSE(out.HasParseError());
}
}
protected:
void TestImportLoadStore(const char* input, const char* expectedImportedValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
rapidjson::Document expectedOutcome;
expectedOutcome.Parse(expectedImportedValue);
ASSERT_FALSE(expectedOutcome.HasParseError());
TestResolveImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutcome.GetObject());
rapidjson::Document originalInput;
originalInput.Parse(input);
ASSERT_FALSE(originalInput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), originalInput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
void TestImportCycle(const char* input)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
AZ::JsonSerializationResult::ResultCode result = TestResolveImports(importerObj);
EXPECT_EQ(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Catastrophic);
m_jsonDocument->SetObject();
delete importerObj;
}
void TestInsertNewImport(const char* input, const char* expectedRestoredValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
TestResolveImports(importerObj);
importerObj->AddImportDirective(rapidjson::Pointer("/object_2"), "object.json");
rapidjson::Document expectedOutput;
expectedOutput.Parse(expectedRestoredValue);
ASSERT_FALSE(expectedOutput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
AZ::JsonSerializationResult::ResultCode TestResolveImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::ResolveImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
AZ::JsonSerializationResult::ResultCode TestRestoreImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::RestoreImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
};
AZ::JsonSerializationResult::ResultCode JsonImporterCustom::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective, const AZ::IO::FixedMaxPath& importedFilePath,
rapidjson::Document::AllocatorType& allocator)
{
AZ::JsonSerializationResult::ResultCode resultCode(AZ::JsonSerializationResult::Tasks::Import);
rapidjson::Document importedDoc;
testClass->GetTestDocument(importedFilePath.String(), importedDoc);
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
return resultCode;
}
// Test Cases
TEST_F(JsonImportingTests, ImportSimpleObjectTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {"$import" : "object.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleObjectPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_2" : "patched_value" }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "patched_value",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {"$import" : "array.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {
"$import" : {
"filename" : "array.json",
"patch" : [ { "element_1" : "patched_value" } ]
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "patched_value" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {"$import" : "nested_import.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportPatchTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {
"$import" : {
"filename" : "nested_import.json",
"patch" : { "obj" : { "field_3" : "patched_value" } }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "patched_value"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportCycleTest)
{
const char* inputFile = R"(
{
"name" : "nested_import_cycle",
"object": {"$import" : "nested_import_c1.json"}
}
)";
TestImportCycle(inputFile);
}
TEST_F(JsonImportingTests, InsertNewImportTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"field_1" : "other_value",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_1" : "other_value" }
}
}
}
)";
TestInsertNewImport(inputFile, expectedOutput);
}
}
@@ -1299,6 +1299,35 @@ namespace SettingsRegistryTests
EXPECT_FALSE(m_registry->MergeCommandLineArgument(" ", {}, {}));
}
//
// MergeSettings
//
TEST_F(SettingsRegistryTest, MergeSettings_MergeJsonWithAnchorKey_StoresSettingsUnderneathKey)
{
constexpr AZStd::string_view anchorKey = "/Anchor/Root/0";
constexpr auto mergeFormat = AZ::SettingsRegistryInterface::Format::JsonMergePatch;
EXPECT_TRUE(m_registry->MergeSettings(R"({ "Test": "1" })", mergeFormat, anchorKey));
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::Array, m_registry->GetType("/Anchor/Root"));
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::Object, m_registry->GetType("/Anchor/Root/0"));
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::String, m_registry->GetType("/Anchor/Root/0/Test"));
}
TEST_F(SettingsRegistryTest, MergeSettings_NotifierSignals_AtAnchorKeyAndStoresMergeType)
{
AZStd::string_view anchorKey = "/Anchor/Root";
bool callbackInvoked{};
auto callback = [anchorKey, &callbackInvoked](AZStd::string_view path, AZ::SettingsRegistryInterface::Type type)
{
EXPECT_EQ(anchorKey, path);
EXPECT_EQ(AZ::SettingsRegistryInterface::Type::Array, type);
callbackInvoked = true;
};
auto testNotifier1 = m_registry->RegisterNotifier(callback);
constexpr auto mergeFormat = AZ::SettingsRegistryInterface::Format::JsonMergePatch;
EXPECT_TRUE(m_registry->MergeSettings(R"([ "Test" ])", mergeFormat, anchorKey));
EXPECT_TRUE(callbackInvoked);
}
//
// MergeSettingsFile
//
@@ -1331,7 +1360,7 @@ namespace SettingsRegistryTests
auto callback = [this](AZStd::string_view path, AZ::SettingsRegistryInterface::Type)
{
EXPECT_TRUE(path.empty());
EXPECT_EQ("/Path", path);
AZ::s64 value = -1;
bool result = m_registry->Get(value, "/Path/Test");
EXPECT_TRUE(result);
@@ -121,6 +121,7 @@ set(FILES
Serialization/Json/TestCases_Classes.cpp
Serialization/Json/TestCases_Compare.cpp
Serialization/Json/TestCases_Enum.cpp
Serialization/Json/TestCases_Importing.cpp
Serialization/Json/TestCases_Patching.cpp
Serialization/Json/TestCases_Pointers.h
Serialization/Json/TestCases_Pointers.cpp
@@ -1924,13 +1924,11 @@ namespace AZ::IO
ArchiveLocationPriority Archive::GetPakPriority() const
{
int pakPriority = aznumeric_cast<int>(ArchiveVars{}.nPriority);
#if defined(AZ_ENABLE_TRACING)
if (auto console = AZ::Interface<AZ::IConsole>::Get(); console != nullptr)
{
AZ::GetValueResult getCvarResult = console->GetCvarValue("sys_PakPriority", pakPriority);
[[maybe_unused]] AZ::GetValueResult getCvarResult = console->GetCvarValue("sys_PakPriority", pakPriority);
AZ_Error("Archive", getCvarResult == AZ::GetValueResult::Success, "Lookup of 'sys_PakPriority console variable failed with error %s", AZ::GetEnumString(getCvarResult));
}
#endif
return static_cast<ArchiveLocationPriority>(pakPriority);
}
@@ -43,7 +43,7 @@ namespace AzFramework
class IMatchmakingAsyncRequests
{
public:
AZ_RTTI(ISessionAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}");
AZ_RTTI(IMatchmakingAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}");
IMatchmakingAsyncRequests() = default;
virtual ~IMatchmakingAsyncRequests() = default;
@@ -60,4 +60,31 @@ namespace AzFramework
// @param stopMatchmakingRequest The request of StopMatchmaking operation
virtual void StopMatchmakingAsync(const StopMatchmakingRequest& stopMatchmakingRequest) = 0;
};
//! MatchmakingAsyncRequestNotifications
//! The notifications correspond to matchmaking async requests
class MatchmakingAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes
virtual void OnAcceptMatchAsyncComplete() = 0;
// OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes
// @param matchmakingTicketId The unique identifier for the matchmaking ticket
virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0;
// OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes
virtual void OnStopMatchmakingAsyncComplete() = 0;
};
using MatchmakingAsyncRequestNotificationBus = AZ::EBus<MatchmakingAsyncRequestNotifications>;
} // namespace AzFramework
@@ -13,36 +13,10 @@
namespace AzFramework
{
//! MatchmakingAsyncRequestNotifications
//! The notifications correspond to matchmaking async requests
class MatchmakingAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes
virtual void OnAcceptMatchAsyncComplete() = 0;
// OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes
// @param matchmakingTicketId The unique identifier for the matchmaking ticket
virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0;
// OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes
virtual void OnStopMatchmakingAsyncComplete() = 0;
};
using MatchmakingAsyncRequestNotificationBus = AZ::EBus<MatchmakingAsyncRequestNotifications>;
//! MatchmakingNotifications
//! The matchmaking notifications to listen for performing required operations
class MatchAcceptanceNotifications
//! based on matchmaking ticket event
class MatchmakingNotifications
: public AZ::EBusTraits
{
public:
@@ -55,8 +29,18 @@ namespace AzFramework
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnMatchAcceptance is fired when DescribeMatchmaking ticket status is REQUIRES_ACCEPTANCE
// OnMatchAcceptance is fired when match is found and pending on acceptance
// Use this notification to accept found match
virtual void OnMatchAcceptance() = 0;
// OnMatchComplete is fired when match is complete
virtual void OnMatchComplete() = 0;
// OnMatchError is fired when match is processed with error
virtual void OnMatchError() = 0;
// OnMatchFailure is fired when match is failed to complete
virtual void OnMatchFailure() = 0;
};
using MatchAcceptanceNotificationBus = AZ::EBus<MatchAcceptanceNotifications>;
using MatchmakingNotificationBus = AZ::EBus<MatchmakingNotifications>;
} // namespace AzFramework
@@ -22,7 +22,7 @@ namespace AzFramework
AZStd::scoped_ptr<ProcessWatcher> pWatcher(LaunchProcess(processLaunchInfo, communicationType));
if (!pWatcher)
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -31,7 +31,7 @@ namespace AzFramework
ProcessCommunicator* pCommunicator = pWatcher->GetCommunicator();
if (!pCommunicator || !pCommunicator->IsValid())
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -30,22 +30,42 @@ namespace AzFramework
//////////////////////////////////////////////////////////////////////////
// OnSessionHealthCheck is fired in health check process
// @return The result of all OnSessionHealthCheck
// Use this notification to perform any custom health check
// @return True if OnSessionHealthCheck succeeds, false otherwise
virtual bool OnSessionHealthCheck() = 0;
// OnCreateSessionBegin is fired at the beginning of session creation
// OnCreateSessionBegin is fired at the beginning of session creation process
// Use this notification to perform any necessary configuration or initialization before
// creating session
// @param sessionConfig The properties to describe a session
// @return The result of all OnCreateSessionBegin notifications
// @return True if OnCreateSessionBegin succeeds, false otherwise
virtual bool OnCreateSessionBegin(const SessionConfig& sessionConfig) = 0;
// OnDestroySessionBegin is fired at the beginning of session termination
// @return The result of all OnDestroySessionBegin notifications
// OnCreateSessionEnd is fired at the end of session creation process
// Use this notification to perform any follow-up operation after session is created and active
virtual void OnCreateSessionEnd() = 0;
// OnDestroySessionBegin is fired at the beginning of session termination process
// Use this notification to perform any cleanup operation before destroying session,
// like gracefully disconnect players, cleanup data, etc.
// @return True if OnDestroySessionBegin succeeds, false otherwise
virtual bool OnDestroySessionBegin() = 0;
// OnUpdateSessionBegin is fired at the beginning of session update
// OnDestroySessionEnd is fired at the end of session termination process
// Use this notification to perform any follow-up operation after session is destroyed,
// like shutdown application process, etc.
virtual void OnDestroySessionEnd() = 0;
// OnUpdateSessionBegin is fired at the beginning of session update process
// Use this notification to perform any configuration or initialization to handle
// the session settings changing
// @param sessionConfig The properties to describe a session
// @param updateReason The reason for session update
virtual void OnUpdateSessionBegin(const SessionConfig& sessionConfig, const AZStd::string& updateReason) = 0;
// OnUpdateSessionBegin is fired at the end of session update process
// Use this notification to perform any follow-up operations after session is updated
virtual void OnUpdateSessionEnd() = 0;
};
using SessionNotificationBus = AZ::EBus<SessionNotifications>;
} // namespace AzFramework
@@ -308,7 +308,7 @@ namespace AzFramework
event.data.data32[2] = 0;
event.data.data32[3] = 1;
event.data.data32[4] = 0;
xcb_void_cookie_t xcbCheckResult = xcb_send_event(
[[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event(
m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
(const char*)&event);
AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set _NET_WM_STATE_FULLSCREEN");
@@ -333,7 +333,7 @@ namespace AzFramework
event.data.data32[2] = 0;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
xcb_void_cookie_t xcbCheckResult = xcb_send_event(
[[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event(
m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
(const char*)&event);
AZ_Assert(
@@ -91,31 +91,42 @@ namespace AzFramework
return processId == 0;
}
/*! Executes a command in the child process after the fork operation has been executed.
* This function will never return. If the execvp command fails this will call _exit with
* the errno value as the return value since continuing execution after a execvp command
* is invalid (it will be running the parent's code and in its address space and will
* cause many issues).
/*! Executes a command in the child process after the fork operation
* has been executed. This function will never return. If the execvpe
* command fails this will call _exit since continuing execution after
* a execvpe command is invalid (it will be running the parent's code
* and in its address space and will cause many issues).
*
* This function runs after a `fork()` call. `fork()` creates a copy of
* the current process, including the current state of the process's
* memory, at the time the call is made. However, it only creates a
* copy of the one thread that called `fork()`. This means that if any
* mutexes are locked by other threads at the time of `fork()`, those
* mutexes will remain locked in the child process, with no way to
* unlock them. So this function needs to ensure that it does as little
* work as possible.
*
* \param commandAndArgs - Array of strings that has the command to execute in index 0 with any args for the command following. Last element must be a null pointer.
* \param envionrmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param environmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param processLaunchInfo - struct containing information about luanching the command
* \param startupInfo - struct containing information needed to startup the command
* \param errorPipe - a pipe file descriptor used to communicate a failed execvpe call's error code to the parent process
*/
void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo)
[[noreturn]] static void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo, const AZStd::array<int, 2>& errorPipe)
{
close(errorPipe[0]);
if (!processLaunchInfo.m_workingDirectory.empty())
{
int res = chdir(processLaunchInfo.m_workingDirectory.c_str());
if (res != 0)
{
std::cerr << strerror(errno) << std::endl;
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to change the launched process' directory to '%s'.", processLaunchInfo.m_workingDirectory.c_str());
write(errorPipe[1], &errno, sizeof(int));
// We *have* to _exit as we are the child process and simply
// returning at this point would mean we would start running
// the code from our parent process and that will just wreck
// havoc.
_exit(errno);
_exit(0);
}
}
@@ -135,15 +146,17 @@ namespace AzFramework
startupInfo.SetupHandlesForChildProcess();
execve(commandAndArgs[0], commandAndArgs, environmentVariables);
execvpe(commandAndArgs[0], commandAndArgs, environmentVariables);
const int errval = errno;
// If we get here then execve failed to run the requested program and
// If we get here then execvpe failed to run the requested program and
// we have an error. In this case we need to exit the child process
// to stop it from continuing to run as a clone of the parent
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process %s : errno = %s ", commandAndArgs[0], strerror(errno));
std::cerr << strerror(errno) << std::endl;
// to stop it from continuing to run as a clone of the parent.
// Communicate the error code back to the parent via a pipe for the
// parent to read.
write(errorPipe[1], &errval, sizeof(errval));
_exit(errno);
_exit(0);
}
}
@@ -212,9 +225,8 @@ namespace AzFramework
AZStd::string outputString;
bool inQuotes = false;
for (size_t pos = 0; pos < processLaunchInfo.m_commandlineParameters.size(); ++pos)
for (const char currentChar : processLaunchInfo.m_commandlineParameters)
{
char currentChar = processLaunchInfo.m_commandlineParameters[pos];
if (currentChar == '"')
{
inQuotes = !inQuotes;
@@ -231,7 +243,7 @@ namespace AzFramework
outputString.push_back(currentChar);
}
}
if (!outputString.empty())
{
commandTokens.push_back(outputString);
@@ -249,10 +261,10 @@ namespace AzFramework
return false;
}
// Because of the way execve is defined we need to copy the strings from
// Because of the way execvpe is defined we need to copy the strings from
// AZ::string (using c_str() returns a const char*) into a non-const char*
// Need to add one more as exec requires the array's last element to be a null pointer
// Need to add one more as execvpe requires the array's last element to be a null pointer
char** commandAndArgs = new char*[commandTokens.size() + 1];
for (int i = 0; i < commandTokens.size(); ++i)
{
@@ -275,7 +287,7 @@ namespace AzFramework
azstrcat(environmentVariable.get(), envVarString.size() + 1, envVarString.c_str());
environmentVariablesVector.emplace_back(environmentVariable.get());
}
// Adding one more as exec expects the array to have a nullptr as the last element
// Adding one more as execvpe expects the array to have a nullptr as the last element
environmentVariablesVector.emplace_back(nullptr);
environmentVariables = environmentVariablesVector.data();
}
@@ -288,15 +300,50 @@ namespace AzFramework
AZ_Assert(environmentVariables, "Environment variables for current process not available\n");
}
// Set up a pipe to communicate the error code from the subprocess's execvpe call
AZStd::array<int, 2> childErrorPipeFds{};
pipe(childErrorPipeFds.data());
// This configures the write end of the pipe to close on calls to `exec`
fcntl(childErrorPipeFds[1], F_SETFD, fcntl(childErrorPipeFds[1], F_GETFD) | FD_CLOEXEC);
pid_t child_pid = fork();
if (IsIdChildProcess(child_pid))
{
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo);
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo, childErrorPipeFds);
}
processData.m_childProcessId = child_pid;
// Close these handles as they are only to be used by the child process
processData.m_startupInfo.CloseAllHandles();
close(childErrorPipeFds[1]);
{
int errorCodeFromChild = 0;
int count = 0;
// Read from the error pipe.
// * If the child's call to execvpe succeeded, then the pipe will
// be closed due to setting FD_CLOEXEC on the write end of the
// pipe. `read()` will return 0.
// * If the child's call to execvpe failed, the child will have
// written the error code to the pipe. `read()` will return >0, and
// the data to be read is the error code from execvpe.
while ((count = read(childErrorPipeFds[0], &errorCodeFromChild, sizeof(errorCodeFromChild))) == -1)
{
if (errno != EAGAIN && errno != EINTR)
{
break;
}
}
if (count)
{
AZ_TracePrintf("Process Watcher", "ProcessLauncher::LaunchProcess: Unable to launch process %s : errno = %s\n", commandAndArgs[0], strerror(errorCodeFromChild));
processData.m_childProcessIsDone = true;
child_pid = -1;
}
}
close(childErrorPipeFds[0]);
processData.m_childProcessId = child_pid;
for (int i = 0; i < commandTokens.size(); i++)
{
@@ -46,6 +46,8 @@ namespace AzManipulatorTestFramework
virtual void UpdateVisibility() = 0;
//! Set if sticky select is enabled or not.
virtual void SetStickySelect(bool enabled) = 0;
//! Get default Editor Camera Position.
virtual AZ::Vector3 DefaultEditorCameraPosition() const = 0;
};
//! This interface is used to simulate the manipulator manager while the manipulators are under test.
@@ -36,6 +36,7 @@ namespace AzManipulatorTestFramework
int GetViewportId() const override;
void UpdateVisibility() override;
void SetStickySelect(bool enabled) override;
AZ::Vector3 DefaultEditorCameraPosition() const override;
// ViewportInteractionRequestBus overrides ...
AzFramework::CameraState GetCameraState() override;
@@ -120,6 +120,11 @@ namespace AzManipulatorTestFramework
m_stickySelect = enabled;
}
AZ::Vector3 ViewportInteraction::DefaultEditorCameraPosition() const
{
return {};
}
void ViewportInteraction::SetGridSize(float size)
{
m_gridSize = size;
@@ -65,7 +65,7 @@ namespace AzNetworking
: m_delta(delta)
, m_dataSerializer(m_delta.GetBufferPtr(), m_delta.GetBufferCapacity())
{
m_namePrefix.reserve(128);
;
}
DeltaSerializerCreate::~DeltaSerializerCreate()
@@ -73,7 +73,7 @@ namespace AzNetworking
// Delete any left over records that might be hanging around
for (auto iter : m_records)
{
delete iter.second;
delete iter;
}
m_records.clear();
}
@@ -160,28 +160,13 @@ namespace AzNetworking
return SerializeHelper(buffer, bufferCapacity, isString, outSize, name);
}
AZStd::string DeltaSerializerCreate::GetNextObjectName(const char* name)
bool DeltaSerializerCreate::BeginObject([[maybe_unused]] const char* name, [[maybe_unused]] const char* typeName)
{
AZStd::string objectName = name;
objectName += ".";
objectName += AZStd::to_string(m_objectCounter);
++m_objectCounter;
return objectName;
}
bool DeltaSerializerCreate::BeginObject(const char* name, [[maybe_unused]] const char* typeName)
{
m_nameLengthStack.push_back(m_namePrefix.length());
m_namePrefix += GetNextObjectName(name);
m_namePrefix += ".";
return true;
}
bool DeltaSerializerCreate::EndObject([[maybe_unused]] const char* name, [[maybe_unused]] const char* typeName)
{
const size_t prevLen = m_nameLengthStack.back();
m_nameLengthStack.pop_back();
m_namePrefix.resize(prevLen);
return true;
}
@@ -205,25 +190,15 @@ namespace AzNetworking
{
typedef AbstractValue::ValueT<T> ValueType;
const size_t prevLen = m_namePrefix.length();
m_namePrefix += GetNextObjectName(name);
const AZ::HashValue32 nameHash = AZ::TypeHash32(m_namePrefix.c_str());
m_namePrefix.resize(prevLen);
AbstractValue::BaseValue*& baseValue = m_records[nameHash];
AbstractValue::BaseValue* baseValue = m_records.size() > m_objectCounter ? m_records[m_objectCounter] : nullptr;
++m_objectCounter;
// If we are in the gather records phase, just save off the value records
if (m_gatheringRecords)
{
if (baseValue != nullptr)
{
AZ_Assert(false, "Duplicate name encountered in delta serializer. This will cause data to be serialized incorrectly.");
return false;
}
AZ_Assert(baseValue == nullptr, "Expected to create a new record but found a pre-existing one at index %d", m_objectCounter - 1);
baseValue = new ValueType(value);
m_records.push_back(baseValue);
}
else // If we are not gathering records, then we are comparing them
{
@@ -90,8 +90,6 @@ namespace AzNetworking
DeltaSerializerCreate(const DeltaSerializerCreate&) = delete;
DeltaSerializerCreate& operator=(const DeltaSerializerCreate&) = delete;
AZStd::string GetNextObjectName(const char* name);
template <typename T>
bool SerializeHelper(T& value, uint32_t bufferCapacity, bool isString, uint32_t& outSize, const char* name);
@@ -105,9 +103,7 @@ namespace AzNetworking
bool m_gatheringRecords = false;
uint32_t m_objectCounter = 0;
AZStd::string m_namePrefix;
AZStd::vector<size_t> m_nameLengthStack;
AZStd::unordered_map<AZ::HashValue32, AbstractValue::BaseValue*> m_records;
AZStd::vector<AbstractValue::BaseValue*> m_records;
NetworkInputSerializer m_dataSerializer;
};
@@ -11,4 +11,213 @@
namespace UnitTest
{
struct DeltaDataElement
{
AzNetworking::PacketId m_packetId = AzNetworking::InvalidPacketId;
uint32_t m_id = 0;
AZ::TimeMs m_timeMs = AZ::TimeMs{ 0 };
float m_blendFactor = 0.f;
AZStd::vector<int> m_growVector, m_shrinkVector;
bool Serialize(AzNetworking::ISerializer& serializer)
{
if (!serializer.Serialize(m_packetId, "PacketId")
|| !serializer.Serialize(m_id, "Id")
|| !serializer.Serialize(m_timeMs, "TimeMs")
|| !serializer.Serialize(m_blendFactor, "BlendFactor")
|| !serializer.Serialize(m_growVector, "GrowVector")
|| !serializer.Serialize(m_shrinkVector, "ShrinkVector"))
{
return false;
}
return true;
}
};
struct DeltaDataContainer
{
AZStd::string m_containerName;
AZStd::array<DeltaDataElement, 32> m_container;
// This logic is modeled after NetworkInputArray serialization in the Multiplayer Gem
bool Serialize(AzNetworking::ISerializer& serializer)
{
// Always serialize the full first element
if(!m_container[0].Serialize(serializer))
{
return false;
}
for (uint32_t i = 1; i < m_container.size(); ++i)
{
if (serializer.GetSerializerMode() == AzNetworking::SerializerMode::WriteToObject)
{
AzNetworking::SerializerDelta deltaSerializer;
// Read out the delta
if (!deltaSerializer.Serialize(serializer))
{
return false;
}
// Start with previous value
m_container[i] = m_container[i - 1];
// Then apply delta
AzNetworking::DeltaSerializerApply applySerializer(deltaSerializer);
if (!applySerializer.ApplyDelta(m_container[i]))
{
return false;
}
}
else
{
AzNetworking::SerializerDelta deltaSerializer;
// Create the delta
AzNetworking::DeltaSerializerCreate createSerializer(deltaSerializer);
if (!createSerializer.CreateDelta(m_container[i - 1], m_container[i]))
{
return false;
}
// Then write out the delta
if (!deltaSerializer.Serialize(serializer))
{
return false;
}
}
}
return true;
}
// This logic is modeled after NetworkInputArray serialization in the Multiplayer Gem
bool SerializeNoDelta(AzNetworking::ISerializer& serializer)
{
for (uint32_t i = 0; i < m_container.size(); ++i)
{
if(!m_container[i].Serialize(serializer))
{
return false;
}
}
return true;
}
};
class DeltaSerializerTests
: public UnitTest::AllocatorsTestFixture
{
public:
void SetUp() override
{
UnitTest::AllocatorsTestFixture::SetUp();
}
void TearDown() override
{
UnitTest::AllocatorsTestFixture::TearDown();
}
};
static constexpr float BLEND_FACTOR_SCALE = 1.1f;
static constexpr uint32_t TIME_SCALE = 10;
DeltaDataContainer TestDeltaContainer()
{
DeltaDataContainer testContainer;
AZStd::vector<int> growVector, shrinkVector;
shrinkVector.resize(testContainer.m_container.array_size);
testContainer.m_containerName = "TestContainer";
for (int i = 0; i < testContainer.m_container.array_size; ++i)
{
testContainer.m_container[i].m_packetId = AzNetworking::PacketId(i);
testContainer.m_container[i].m_id = i;
testContainer.m_container[i].m_timeMs = AZ::TimeMs(i * TIME_SCALE);
testContainer.m_container[i].m_blendFactor = BLEND_FACTOR_SCALE * i;
growVector.push_back(i);
testContainer.m_container[i].m_growVector = growVector;
shrinkVector.resize(testContainer.m_container.array_size - i);
testContainer.m_container[i].m_shrinkVector = shrinkVector;
}
return testContainer;
}
TEST_F(DeltaSerializerTests, DeltaArray)
{
DeltaDataContainer inContainer = TestDeltaContainer();
AZStd::array<uint8_t, 2048> buffer;
AzNetworking::NetworkInputSerializer inSerializer(buffer.data(), static_cast<uint32_t>(buffer.size()));
// Always serialize the full first element
EXPECT_TRUE(inContainer.Serialize(inSerializer));
DeltaDataContainer outContainer;
AzNetworking::NetworkOutputSerializer outSerializer(buffer.data(), static_cast<uint32_t>(buffer.size()));
EXPECT_TRUE(outContainer.Serialize(outSerializer));
for (uint32_t i = 0; i > outContainer.m_container.size(); ++i)
{
EXPECT_EQ(inContainer.m_container[i].m_blendFactor, outContainer.m_container[i].m_blendFactor);
EXPECT_EQ(inContainer.m_container[i].m_id, outContainer.m_container[i].m_id);
EXPECT_EQ(inContainer.m_container[i].m_packetId, outContainer.m_container[i].m_packetId);
EXPECT_EQ(inContainer.m_container[i].m_timeMs, outContainer.m_container[i].m_timeMs);
EXPECT_EQ(inContainer.m_container[i].m_growVector[i], outContainer.m_container[i].m_growVector[i]);
EXPECT_EQ(inContainer.m_container[i].m_growVector.size(), outContainer.m_container[i].m_growVector.size());
EXPECT_EQ(inContainer.m_container[i].m_shrinkVector.size(), outContainer.m_container[i].m_shrinkVector.size());
}
}
TEST_F(DeltaSerializerTests, DeltaSerializerCreateUnused)
{
// Every function here should return a constant value regardless of inputs
AzNetworking::SerializerDelta deltaSerializer;
AzNetworking::DeltaSerializerCreate createSerializer(deltaSerializer);
EXPECT_EQ(createSerializer.GetCapacity(), 0);
EXPECT_EQ(createSerializer.GetSize(), 0);
EXPECT_EQ(createSerializer.GetBuffer(), nullptr);
EXPECT_EQ(createSerializer.GetSerializerMode(), AzNetworking::SerializerMode::ReadFromObject);
createSerializer.ClearTrackedChangesFlag(); //NO-OP
EXPECT_FALSE(createSerializer.GetTrackedChangesFlag());
EXPECT_TRUE(createSerializer.BeginObject("CreateSerializer", "Begin"));
EXPECT_TRUE(createSerializer.EndObject("CreateSerializer", "End"));
}
TEST_F(DeltaSerializerTests, DeltaArraySize)
{
DeltaDataContainer deltaContainer = TestDeltaContainer();
DeltaDataContainer noDeltaContainer = TestDeltaContainer();
AZStd::array<uint8_t, 2048> deltaBuffer;
AzNetworking::NetworkInputSerializer deltaSerializer(deltaBuffer.data(), static_cast<uint32_t>(deltaBuffer.size()));
AZStd::array<uint8_t, 2048> noDeltaBuffer;
AzNetworking::NetworkInputSerializer noDeltaSerializer(noDeltaBuffer.data(), static_cast<uint32_t>(noDeltaBuffer.size()));
EXPECT_TRUE(deltaContainer.Serialize(deltaSerializer));
EXPECT_FALSE(noDeltaContainer.SerializeNoDelta(noDeltaSerializer)); // Should run out of space
EXPECT_EQ(noDeltaSerializer.GetCapacity(), noDeltaSerializer.GetSize()); // Verify that the serializer filled up
EXPECT_FALSE(noDeltaSerializer.IsValid()); // and that it is no longer valid due to lack of space
}
TEST_F(DeltaSerializerTests, DeltaSerializerApplyUnused)
{
// Every function here should return a constant value regardless of inputs
AzNetworking::SerializerDelta deltaSerializer;
AzNetworking::DeltaSerializerApply applySerializer(deltaSerializer);
EXPECT_EQ(applySerializer.GetCapacity(), 0);
EXPECT_EQ(applySerializer.GetSize(), 0);
EXPECT_EQ(applySerializer.GetBuffer(), nullptr);
EXPECT_EQ(applySerializer.GetSerializerMode(), AzNetworking::SerializerMode::WriteToObject);
applySerializer.ClearTrackedChangesFlag(); //NO-OP
EXPECT_FALSE(applySerializer.GetTrackedChangesFlag());
EXPECT_TRUE(applySerializer.BeginObject("CreateSerializer", "Begin"));
EXPECT_TRUE(applySerializer.EndObject("CreateSerializer", "End"));
}
}
@@ -0,0 +1,196 @@
/*
* 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 <AzCore/Component/Entity.h>
#include <AzQtComponents/Components/ToastNotification.h>
#include <AzQtComponents/Components/ui_ToastNotification.h>
#include <QCursor>
#include <QIcon>
#include <QToolButton>
#include <QPropertyAnimation>
namespace AzQtComponents
{
ToastNotification::ToastNotification(QWidget* parent, const ToastConfiguration& toastConfiguration)
: QDialog(parent, Qt::FramelessWindowHint)
, m_closeOnClick(true)
, m_ui(new Ui::ToastNotification())
, m_fadeAnimation(nullptr)
{
setProperty("HasNoWindowDecorations", true);
setAttribute(Qt::WA_ShowWithoutActivating);
setAttribute(Qt::WA_DeleteOnClose);
m_ui->setupUi(this);
QIcon toastIcon;
switch (toastConfiguration.m_toastType)
{
case ToastType::Error:
toastIcon = QIcon(":/stylesheet/img/logging/error.svg");
break;
case ToastType::Warning:
toastIcon = QIcon(":/stylesheet/img/logging/warning-yellow.svg");
break;
case ToastType::Information:
toastIcon = QIcon(":/stylesheet/img/logging/information.svg");
break;
case ToastType::Custom:
toastIcon = QIcon(toastConfiguration.m_customIconImage);
default:
break;
}
m_ui->iconLabel->setPixmap(toastIcon.pixmap(64, 64));
m_ui->titleLabel->setText(toastConfiguration.m_title);
m_ui->mainLabel->setText(toastConfiguration.m_description);
m_lifeSpan.setInterval(aznumeric_cast<int>(toastConfiguration.m_duration.count()));
m_closeOnClick = toastConfiguration.m_closeOnClick;
m_ui->closeButton->setVisible(m_closeOnClick);
QObject::connect(m_ui->closeButton, &QToolButton::clicked, this, &ToastNotification::accept);
m_fadeDuration = toastConfiguration.m_fadeDuration;
QObject::connect(&m_lifeSpan, &QTimer::timeout, this, &ToastNotification::FadeOut);
}
ToastNotification::~ToastNotification()
{
}
void ToastNotification::ShowToastAtCursor()
{
QPoint globalCursorPos = QCursor::pos();
// Left/middle align it relative to the cursor.
QPointF anchorPoint(0, 0.5);
// Magic offset to try to get it to not hide under the cursor.
// No way to get this programatically from what I can tell.
globalCursorPos.setX(globalCursorPos.x() + 16);
ShowToastAtPoint(globalCursorPos, anchorPoint);
}
void ToastNotification::ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint)
{
show();
updateGeometry();
UpdatePosition(screenPosition, anchorPoint);
}
void ToastNotification::UpdatePosition(const QPoint& screenPosition, const QPointF& anchorPoint)
{
QRect dialogGeometry = geometry();
QPoint finalPosition;
finalPosition.setX(aznumeric_cast<int>(screenPosition.x() - dialogGeometry.width() * anchorPoint.x()));
finalPosition.setY(aznumeric_cast<int>(screenPosition.y() - dialogGeometry.height() * anchorPoint.y()));
move(finalPosition);
}
void ToastNotification::showEvent(QShowEvent* showEvent)
{
QDialog::showEvent(showEvent);
if (m_fadeDuration.count() > 0)
{
m_fadeAnimation = new QPropertyAnimation(this, "windowOpacity", this);
m_fadeAnimation->setKeyValueAt(0, 0);
m_fadeAnimation->setKeyValueAt(1, 1);
m_fadeAnimation->setDuration(static_cast<int>(m_fadeDuration.count()));
m_fadeAnimation->start();
QObject::connect(m_fadeAnimation, &QPropertyAnimation::finished, this, &ToastNotification::StartTimer);
}
else
{
StartTimer();
}
}
void ToastNotification::hideEvent(QHideEvent* hideEvent)
{
QDialog::hideEvent(hideEvent);
m_lifeSpan.stop();
if (m_fadeAnimation)
{
m_fadeAnimation->stop();
delete m_fadeAnimation;
}
emit ToastNotificationHidden();
}
void ToastNotification::mousePressEvent(QMouseEvent*)
{
if (m_closeOnClick)
{
emit ToastNotificationInteraction();
accept();
}
}
bool ToastNotification::eventFilter(QObject*, QEvent* event)
{
if (event->type() == QEvent::MouseButtonPress)
{
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent && mouseEvent->button() == Qt::MouseButton::LeftButton)
{
accept();
}
}
return false;
}
void ToastNotification::StartTimer()
{
delete m_fadeAnimation;
m_fadeAnimation = nullptr;
if (m_lifeSpan.interval() != 0)
{
m_lifeSpan.start();
}
}
void ToastNotification::FadeOut()
{
if (m_fadeDuration.count() > 0)
{
m_fadeAnimation = new QPropertyAnimation(this, "windowOpacity", this);
m_fadeAnimation->setKeyValueAt(0, windowOpacity());
m_fadeAnimation->setKeyValueAt(1, 0);
m_fadeAnimation->setDuration(static_cast<int>(m_fadeDuration.count()));
m_fadeAnimation->start();
QObject::connect(m_fadeAnimation, &QPropertyAnimation::finished, this, &ToastNotification::accept);
}
else
{
accept();
}
}
#include "Components/moc_ToastNotification.cpp"
}
@@ -0,0 +1,74 @@
/*
* 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
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzQtComponents/AzQtComponentsAPI.h>
#include <AzQtComponents/Components/ToastNotificationConfiguration.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <QEvent>
#include <QDialog>
#include <QMouseEvent>
#include <QTimer>
#endif
namespace Ui
{
class ToastNotification;
}
QT_FORWARD_DECLARE_CLASS(QPropertyAnimation)
namespace AzQtComponents
{
class AZ_QT_COMPONENTS_API ToastNotification
: public QDialog
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(ToastNotification, AZ::SystemAllocator, 0);
ToastNotification(QWidget* parent, const ToastConfiguration& toastConfiguration);
virtual ~ToastNotification();
// Shows the toast notification relative to the current cursor.
void ShowToastAtCursor();
// Aligns the toast notification so that the specified anchor point on the notification lies on the specified screen position.
// i.e. anchor point of 0,0 will align the top left position of the dialog with the screen position
// anchor point of 1,1 will align the bottom right position of the dialog with the screen position
void ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint);
void UpdatePosition(const QPoint& screenPosition, const QPointF& anchorPoint);
// QDialog
void showEvent(QShowEvent* showEvent) override;
void hideEvent(QHideEvent* hideEvent) override;
void mousePressEvent(QMouseEvent* mouseEvent) override;
bool eventFilter(QObject* object, QEvent* event) override;
public slots:
void StartTimer();
void FadeOut();
signals:
void ToastNotificationHidden();
void ToastNotificationInteraction();
private:
QPropertyAnimation* m_fadeAnimation;
bool m_closeOnClick;
QTimer m_lifeSpan;
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
AZStd::chrono::milliseconds m_fadeDuration;
AZStd::unique_ptr<Ui::ToastNotification> m_ui;
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
};
} // namespace AzQtComponents
@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ToastNotification</class>
<widget class="QDialog" name="ToastNotification">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>225</width>
<height>48</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>225</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="icon_frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgba(255, 255, 255, 20);</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="iconLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgba(255, 255, 255, 0);</string>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources.qrc">:/stylesheet/img/logging/information.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="text_frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="titleFrame">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="titleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Invalid Connection</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="closeButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/stylesheet/img/close_x.svg</normaloff>:/stylesheet/img/close_x.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="mainLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Types are not a match.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,18 @@
/*
* 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 <AzQtComponents/Components/ToastNotificationConfiguration.h>
namespace AzQtComponents
{
ToastConfiguration::ToastConfiguration(ToastType toastType, const QString& title, const QString& description)
: m_toastType(toastType)
, m_title(title)
, m_description(description)
{
}
} // namespace AzQtComponents
@@ -0,0 +1,46 @@
/*
* 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
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzQtComponents/AzQtComponentsAPI.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/chrono/chrono.h>
#include <QString>
#endif
namespace AzQtComponents
{
enum class ToastType
{
Information,
Warning,
Error,
Custom
};
class AZ_QT_COMPONENTS_API ToastConfiguration
{
public:
AZ_CLASS_ALLOCATOR(ToastConfiguration, AZ::SystemAllocator, 0);
ToastConfiguration(ToastType toastType, const QString& title, const QString& description);
bool m_closeOnClick = true;
ToastType m_toastType = ToastType::Information;
QString m_title;
QString m_description;
QString m_customIconImage;
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds(5000);
AZStd::chrono::milliseconds m_fadeDuration = AZStd::chrono::milliseconds(250);
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
};
} // namespace AzQtComponents
@@ -49,6 +49,11 @@ set(FILES
Components/Titlebar.h
Components/TitleBarOverdrawHandler.cpp
Components/TitleBarOverdrawHandler.h
Components/ToastNotification.cpp
Components/ToastNotification.h
Components/ToastNotificationConfiguration.h
Components/ToastNotificationConfiguration.cpp
Components/ToastNotification.ui
Components/ToolButtonComboBox.cpp
Components/ToolButtonComboBox.h
Components/ToolButtonLineEdit.cpp
@@ -7,6 +7,7 @@
*/
#include <dlfcn.h>
#include <iostream>
#include <AzCore/IO/Path/Path.h>
#include <AzTest/Platform.h>
#include <sys/types.h>
@@ -20,11 +21,16 @@ public:
explicit ModuleHandle(const std::string& lib)
: m_libHandle(nullptr)
{
std::string libext = lib;
if (!AZ::Test::EndsWith(libext, ".dylib"))
AZ::IO::FixedMaxPath libext = AZStd::string_view{ lib.c_str(), lib.size() };
if (!libext.Stem().Native().starts_with(AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX))
{
libext += ".dylib";
libext = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX + libext.Native();
}
if (libext.Extension() != AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION)
{
libext.Native() += AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
}
m_libHandle = dlopen(libext.c_str(), RTLD_NOW);
const char* error = dlerror();
if (error)
@@ -381,22 +381,13 @@ namespace AzToolsFramework
return;
}
bool isPrefabSystemEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
isPrefabSystemEnabled, &AzFramework::ApplicationRequests::IsPrefabSystemEnabled);
// For slices, orphan any children that remain attached to the entity
// For prefabs, this is an unneeded operation because the prefab system handles the orphans
// and the extra reparenting operation can be problematic for consumers subscribed to entity
// events, such as the entity outliner.
if (!isPrefabSystemEnabled)
// Even though these child entities will immediately be destroyed, their entity info may be recycled
// Ensure they don't have any lingering inaccurate parent data
auto children = entityInfo.GetChildren();
for (auto childId : children)
{
auto children = entityInfo.GetChildren();
for (auto childId : children)
{
ReparentChild(childId, AZ::EntityId(), entityId);
m_entityOrphanTable[entityId].insert(childId);
}
ReparentChild(childId, AZ::EntityId(), entityId);
m_entityOrphanTable[entityId].insert(childId);
}
m_savedOrderInfo[entityId] = AZStd::make_pair(entityInfo.GetParent(), entityInfo.GetIndexForSorting());
@@ -1200,26 +1191,41 @@ namespace AzToolsFramework
auto childItr = m_childIndexCache.find(childId);
if (childItr == m_childIndexCache.end())
{
//cache indices for faster lookup
m_childIndexCache[childId] = static_cast<AZ::u64>(m_children.size());
m_children.push_back(childId);
// m_children is guaranteed to be ordered by EntityId, do a sorted insertion
auto insertedChildIndex = AZStd::upper_bound(m_children.begin(), m_children.end(), childId);
insertedChildIndex = m_children.insert(insertedChildIndex, childId);
// Cache all affected child indices for fast lookup
for (auto it = insertedChildIndex; it != m_children.end(); ++it)
{
const AZ::u64 newChildIndex = static_cast<AZ::u64>(it - m_children.begin());
m_childIndexCache[*it] = newChildIndex;
}
}
}
void EditorEntityModel::EditorEntityModelEntry::RemoveChild(AZ::EntityId childId)
{
auto childItr = m_childIndexCache.find(childId);
if (childItr != m_childIndexCache.end())
// Retrieve our child index from the cache
auto cachedIndexItr = m_childIndexCache.find(childId);
if (cachedIndexItr == m_childIndexCache.end())
{
// Take the last entry and move it into the removed spot instead of deleting the entry and having to move all
// following entries one step down.
AZ::EntityId backEntity = m_children.back();
m_children[childItr->second] = backEntity;
// Update cached index for the moved id to the new index.
m_childIndexCache[backEntity] = childItr->second;
// Now remove the deleted id from the children and cache.
m_childIndexCache.erase(childId);
m_children.erase(m_children.end() - 1);
AZ_Assert(false, "Attempted to remove an unknown child");
return;
}
// Build an iterator for m_children based on our cached index
auto childItr = m_children.begin() + cachedIndexItr->second;
// Remove our child from the cache
m_childIndexCache.erase(cachedIndexItr);
// Remove our child, fix up the cache entries for any subsequent children
auto elementsToFixItr = m_children.erase(childItr);
for (auto it = elementsToFixItr; it != m_children.end(); ++it)
{
const AZ::u64 newChildIndex = static_cast<AZ::u64>(it - m_children.begin());
m_childIndexCache[*it] = newChildIndex;
}
}
@@ -1256,8 +1262,17 @@ namespace AzToolsFramework
AZ::u64 EditorEntityModel::EditorEntityModelEntry::GetChildIndex(AZ::EntityId childId) const
{
// Return the cached index, if available.
auto childItr = m_childIndexCache.find(childId);
return childItr != m_childIndexCache.end() ? childItr->second : static_cast<AZ::u64>(m_children.size());
if (childItr != m_childIndexCache.end())
{
return childItr->second;
}
// On initialization, GetChildIndex may be queried for a childId that is not yet in the child list.
// Return the position it would be inserted at in EditorEntityModelEntry::AddChild
auto targetChildPositionItr = AZStd::upper_bound(m_children.begin(), m_children.end(), childId);
return static_cast<AZ::u64>(targetChildPositionItr - m_children.begin());
}
AZStd::string EditorEntityModel::EditorEntityModelEntry::GetName() const
@@ -53,7 +53,7 @@ namespace AzToolsFramework
AZ_Assert(m_loaderInterface != nullptr,
"Couldn't get prefab loader interface, it's a requirement for PrefabEntityOwnership system to work");
m_rootInstance = AZStd::unique_ptr<Prefab::Instance>(m_prefabSystemComponent->CreatePrefab({}, {}, "NewLevel.prefab"));
m_rootInstance = AZStd::unique_ptr<Prefab::Instance>(m_prefabSystemComponent->CreatePrefab({}, {}, "newLevel.prefab"));
m_sliceOwnershipService.BusConnect(m_entityContextId);
m_sliceOwnershipService.m_shouldAssertForLegacySlicesUsage = m_shouldAssertForLegacySlicesUsage;
m_editorSliceOwnershipService.BusConnect();
@@ -244,6 +244,17 @@ namespace AzToolsFramework
const auto eventType = event->type();
if (eventType == QEvent::Type::MouseMove)
{
// clear override cursor when moving outside of the viewport
const auto* mouseEvent = static_cast<const QMouseEvent*>(event);
if (m_overrideCursor && !m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(mouseEvent->globalPos())))
{
qApp->restoreOverrideCursor();
m_overrideCursor = false;
}
}
// Only accept mouse & key release events that originate from an object that is not our target widget,
// as we don't want to erroneously intercept user input meant for another component.
if (object != m_sourceWidget && eventType != QEvent::Type::KeyRelease && eventType != QEvent::Type::MouseButtonRelease)
@@ -262,7 +273,7 @@ namespace AzToolsFramework
if (eventType == QEvent::FocusIn)
{
const auto globalCursorPosition = QCursor::pos();
if (m_sourceWidget->geometry().contains(globalCursorPosition))
if (m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(globalCursorPosition)))
{
HandleMouseMoveEvent(globalCursorPosition);
}
@@ -452,4 +463,32 @@ namespace AzToolsFramework
}
}
}
static Qt::CursorShape QtCursorFromAzCursor(const ViewportInteraction::CursorStyleOverride cursorStyleOverride)
{
switch (cursorStyleOverride)
{
case ViewportInteraction::CursorStyleOverride::Forbidden:
return Qt::ForbiddenCursor;
default:
return Qt::ArrowCursor;
}
}
void QtEventToAzInputMapper::SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride)
{
ClearOverrideCursor();
qApp->setOverrideCursor(QtCursorFromAzCursor(cursorStyleOverride));
m_overrideCursor = true;
}
void QtEventToAzInputMapper::ClearOverrideCursor()
{
if (m_overrideCursor)
{
qApp->restoreOverrideCursor();
m_overrideCursor = false;
}
}
} // namespace AzToolsFramework
@@ -15,10 +15,11 @@
#include <AzFramework/Input/Channels/InputChannelDeltaWithSharedPosition2D.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedModifierKeyStates.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedPosition2D.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <QEvent>
#include <QObject>
#include <QPoint>
@@ -55,6 +56,9 @@ namespace AzToolsFramework
//! like a dolly or rotation, where mouse movement is important but cursor location is not.
void SetCursorCaptureEnabled(bool enabled);
void SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride);
void ClearOverrideCursor();
// QObject overrides...
bool eventFilter(QObject* object, QEvent* event) override;
@@ -164,6 +168,8 @@ namespace AzToolsFramework
bool m_enabled = true;
// Flags whether or not the cursor is being constrained to the source widget (for invisible mouse movement).
bool m_capturingCursor = false;
// Flags whether the cursor has been overridden.
bool m_overrideCursor = false;
// Our viewport-specific AZ devices. We control their internal input channel states.
AZStd::unique_ptr<EditorQtMouseDevice> m_mouseDevice;
@@ -28,7 +28,9 @@ namespace AzToolsFramework::Prefab
"Instance Entity Mapper Interface could not be found. "
"Check that it is being correctly initialized.");
EditorEntityInfoNotificationBus::Handler::BusConnect();
EditorEntityContextNotificationBus::Handler::BusConnect();
PrefabPublicNotificationBus::Handler::BusConnect();
AZ::Interface<PrefabFocusInterface>::Register(this);
AZ::Interface<PrefabFocusPublicInterface>::Register(this);
}
@@ -37,10 +39,12 @@ namespace AzToolsFramework::Prefab
{
AZ::Interface<PrefabFocusPublicInterface>::Unregister(this);
AZ::Interface<PrefabFocusInterface>::Unregister(this);
PrefabPublicNotificationBus::Handler::BusDisconnect();
EditorEntityContextNotificationBus::Handler::BusDisconnect();
EditorEntityInfoNotificationBus::Handler::BusDisconnect();
}
void PrefabFocusHandler::Initialize()
void PrefabFocusHandler::InitializeEditorInterfaces()
{
m_containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get();
AZ_Assert(
@@ -55,13 +59,6 @@ namespace AzToolsFramework::Prefab
"Prefab - PrefabFocusHandler - "
"Focus Mode Interface could not be found. "
"Check that it is being correctly initialized.");
m_instanceEntityMapperInterface = AZ::Interface<InstanceEntityMapperInterface>::Get();
AZ_Assert(
m_instanceEntityMapperInterface,
"Prefab - PrefabFocusHandler - "
"Instance Entity Mapper Interface could not be found. "
"Check that it is being correctly initialized.");
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnOwningPrefab(AZ::EntityId entityId)
@@ -90,12 +87,12 @@ namespace AzToolsFramework::Prefab
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPathIndex([[maybe_unused]] AzFramework::EntityContextId entityContextId, int index)
{
if (index < 0 || index >= m_instanceFocusVector.size())
if (index < 0 || index >= m_instanceFocusHierarchy.size())
{
return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex."));
}
InstanceOptionalReference focusedInstance = m_instanceFocusVector[index];
InstanceOptionalReference focusedInstance = m_instanceFocusHierarchy[index];
FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId());
@@ -134,42 +131,38 @@ namespace AzToolsFramework::Prefab
return AZ::Failure(AZStd::string("Prefab Focus Handler: invalid instance to focus on."));
}
if (!m_isInitialized)
// Close all container entities in the old path.
CloseInstanceContainers(m_instanceFocusHierarchy);
m_focusedInstance = focusedInstance;
m_focusedTemplateId = focusedInstance->get().GetTemplateId();
AZ::EntityId containerEntityId;
if (focusedInstance->get().GetParentInstance() != AZStd::nullopt)
{
Initialize();
containerEntityId = focusedInstance->get().GetContainerEntityId();
}
if (!m_focusedInstance.has_value() || &m_focusedInstance->get() != &focusedInstance->get())
else
{
// Close all container entities in the old path
CloseInstanceContainers(m_instanceFocusVector);
m_focusedInstance = focusedInstance;
m_focusedTemplateId = focusedInstance->get().GetTemplateId();
AZ::EntityId containerEntityId;
if (focusedInstance->get().GetParentInstance() != AZStd::nullopt)
{
containerEntityId = focusedInstance->get().GetContainerEntityId();
}
else
{
containerEntityId = AZ::EntityId();
}
containerEntityId = AZ::EntityId();
}
// Focus on the descendants of the container entity
// Focus on the descendants of the container entity in the Editor, if the interface is initialized.
if (m_focusModeInterface)
{
m_focusModeInterface->SetFocusRoot(containerEntityId);
// Refresh path variables
RefreshInstanceFocusList();
// Open all container entities in the new path
OpenInstanceContainers(m_instanceFocusVector);
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
// Refresh path variables.
RefreshInstanceFocusList();
RefreshInstanceFocusPath();
// Open all container entities in the new path.
OpenInstanceContainers(m_instanceFocusHierarchy);
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
return AZ::Success();
}
@@ -220,49 +213,80 @@ namespace AzToolsFramework::Prefab
const int PrefabFocusHandler::GetPrefabFocusPathLength([[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
return aznumeric_cast<int>(m_instanceFocusVector.size());
return aznumeric_cast<int>(m_instanceFocusHierarchy.size());
}
void PrefabFocusHandler::OnEntityStreamLoadSuccess()
void PrefabFocusHandler::OnContextReset()
{
if (!m_isInitialized)
{
Initialize();
}
// Clear the old focus vector
m_instanceFocusVector.clear();
m_instanceFocusHierarchy.clear();
// Focus on the root prefab (AZ::EntityId() will default to it)
FocusOnPrefabInstanceOwningEntityId(AZ::EntityId());
}
void PrefabFocusHandler::OnEntityInfoUpdatedName(AZ::EntityId entityId, [[maybe_unused]]const AZStd::string& name)
{
// Determine if the entityId is the container for any of the instances in the vector
auto result = AZStd::find_if(
m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(),
[entityId](const InstanceOptionalReference& instance)
{
return (instance->get().GetContainerEntityId() == entityId);
}
);
if (result != m_instanceFocusHierarchy.end())
{
// Refresh the path and notify changes.
RefreshInstanceFocusPath();
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
}
void PrefabFocusHandler::OnPrefabInstancePropagationEnd()
{
// Refresh the path and notify changes in case propagation updated any container names.
RefreshInstanceFocusPath();
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
void PrefabFocusHandler::RefreshInstanceFocusList()
{
m_instanceFocusVector.clear();
m_instanceFocusPath.clear();
m_instanceFocusHierarchy.clear();
AZStd::list<InstanceOptionalReference> instanceFocusList;
// Use a support list to easily push front while traversing the prefab hierarchy
InstanceOptionalReference currentInstance = m_focusedInstance;
while (currentInstance.has_value())
{
instanceFocusList.push_front(currentInstance);
m_instanceFocusHierarchy.emplace_back(currentInstance);
currentInstance = currentInstance->get().GetParentInstance();
}
// Populate internals using the support list
for (auto& instance : instanceFocusList)
// Invert the vector, since we need the top instance to be at index 0
AZStd::reverse(m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end());
}
void PrefabFocusHandler::RefreshInstanceFocusPath()
{
m_instanceFocusPath.clear();
for (const InstanceOptionalReference& instance : m_instanceFocusHierarchy)
{
m_instanceFocusPath.Append(instance->get().GetContainerEntity()->get().GetName());
m_instanceFocusVector.emplace_back(instance);
}
}
void PrefabFocusHandler::OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
{
return;
}
for (const InstanceOptionalReference& instance : instances)
{
if (instance.has_value())
@@ -274,6 +298,12 @@ namespace AzToolsFramework::Prefab
void PrefabFocusHandler::CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
{
return;
}
for (const InstanceOptionalReference& instance : instances)
{
if (instance.has_value())
@@ -11,9 +11,11 @@
#include <AzCore/Memory/SystemAllocator.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabPublicNotificationBus.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
namespace AzToolsFramework
@@ -30,7 +32,9 @@ namespace AzToolsFramework::Prefab
class PrefabFocusHandler final
: private PrefabFocusInterface
, private PrefabFocusPublicInterface
, private PrefabPublicNotificationBus::Handler
, private EditorEntityContextNotificationBus::Handler
, private EditorEntityInfoNotificationBus::Handler
{
public:
AZ_CLASS_ALLOCATOR(PrefabFocusHandler, AZ::SystemAllocator, 0);
@@ -38,9 +42,8 @@ namespace AzToolsFramework::Prefab
PrefabFocusHandler();
~PrefabFocusHandler();
void Initialize();
// PrefabFocusInterface overrides ...
void InitializeEditorInterfaces() override;
PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) override;
TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const override;
InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const override;
@@ -54,25 +57,34 @@ namespace AzToolsFramework::Prefab
const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override;
// EditorEntityContextNotificationBus overrides ...
void OnEntityStreamLoadSuccess() override;
void OnContextReset() override;
// EditorEntityInfoNotificationBus overrides ...
void OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZStd::string& name) override;
// PrefabPublicNotifications overrides ...
void OnPrefabInstancePropagationEnd();
private:
PrefabFocusOperationResult FocusOnPrefabInstance(InstanceOptionalReference focusedInstance);
void RefreshInstanceFocusList();
void RefreshInstanceFocusPath();
void OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
void CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
//! The instance the editor is currently focusing on.
InstanceOptionalReference m_focusedInstance;
//! The templateId of the focused instance.
TemplateId m_focusedTemplateId;
AZStd::vector<InstanceOptionalReference> m_instanceFocusVector;
//! The list of instances going from the root (index 0) to the focused instance.
AZStd::vector<InstanceOptionalReference> m_instanceFocusHierarchy;
//! A path containing the names of the containers in the instance focus hierarchy, separated with a /.
AZ::IO::Path m_instanceFocusPath;
ContainerEntityInterface* m_containerEntityInterface = nullptr;
FocusModeInterface* m_focusModeInterface = nullptr;
InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr;
bool m_isInitialized = false;
};
} // namespace AzToolsFramework::Prefab
@@ -26,6 +26,11 @@ namespace AzToolsFramework::Prefab
public:
AZ_RTTI(PrefabFocusInterface, "{F3CFA37B-5FD8-436A-9C30-60EB54E350E1}");
//! Initializes the editor interfaces for Prefab Focus mode.
//! If this is not called on initialization, the Prefab Focus Mode functions will still work
//! but won't trigger the Editor APIs to visualize focus mode on the UI.
virtual void InitializeEditorInterfaces() = 0;
//! Set the focused prefab instance to the owning instance of the entityId provided.
//! @param entityId The entityId of the entity whose owning instance we want the prefab system to focus on.
virtual PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) = 0;
@@ -66,7 +66,16 @@ namespace AzToolsFramework
bool Template::IsValid() const
{
return !m_prefabDom.IsNull() && !m_filePath.empty();
if (m_prefabDom.IsNull() || m_filePath.empty())
{
return false;
}
else if (!m_prefabDom.IsObject())
{
return false;
}
auto source = m_prefabDom.FindMember(PrefabDomUtils::SourceName);
return (source != m_prefabDom.MemberEnd());
}
bool Template::IsLoadedWithErrors() const
@@ -175,6 +184,26 @@ namespace AzToolsFramework
return findInstancesResult->get();
}
bool Template::IsProcedural() const
{
if (m_isProcedural.has_value())
{
return m_isProcedural.value();
}
else if (!IsValid())
{
return false;
}
auto source = m_prefabDom.FindMember(PrefabDomUtils::SourceName);
if (!source->value.IsString())
{
return false;
}
AZ::IO::PathView path(source->value.GetString());
m_isProcedural = AZStd::make_optional(path.Extension().Match(".procprefab"));
return m_isProcedural.value();
}
const AZ::IO::Path& Template::GetFilePath() const
{
return m_filePath;
@@ -65,6 +65,9 @@ namespace AzToolsFramework
const AZ::IO::Path& GetFilePath() const;
void SetFilePath(const AZ::IO::PathView& path);
// To tell if this Template was created from an product asset
bool IsProcedural() const;
private:
// Container for keeping links representing the Template's nested instances.
Links m_links;
@@ -80,6 +83,9 @@ namespace AzToolsFramework
// Flag to tell if this Template has changes that have yet to be saved to file.
bool m_isDirty = false;
// Flag to tell if this Template was generated outside the Editor
mutable AZStd::optional<bool> m_isProcedural;
};
} // namespace Prefab
} // namespace AzToolsFramework
@@ -20,6 +20,8 @@
#include <AzFramework/Process/ProcessWatcher.h>
#include <AzToolsFramework/SourceControl/PerforceConnection.h>
#include <QProcess>
namespace AzToolsFramework
{
namespace
@@ -75,9 +77,17 @@ namespace AzToolsFramework
m_resolveKey = true;
m_testTrust = false;
// set up signals before we start thread.
m_shutdownThreadSignal = false;
m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this));
// Check to see if the 'p4' command is available at the command line
int p4VersionExitCode = QProcess::execute("p4", QStringList{ "-V" });
m_p4ApplicationDetected = (p4VersionExitCode == 0);
if (m_p4ApplicationDetected)
{
m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this));
}
SourceControlConnectionRequestBus::Handler::BusConnect();
SourceControlCommandBus::Handler::BusConnect();
@@ -88,10 +98,13 @@ namespace AzToolsFramework
SourceControlCommandBus::Handler::BusDisconnect();
SourceControlConnectionRequestBus::Handler::BusDisconnect();
m_shutdownThreadSignal = true; // tell the thread to die.
m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal
m_WorkerThread.join(); // wait for the thread to finish.
m_WorkerThread = AZStd::thread();
if (m_p4ApplicationDetected)
{
m_shutdownThreadSignal = true; // tell the thread to die.
m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal
m_WorkerThread.join(); // wait for the thread to finish.
m_WorkerThread = AZStd::thread();
}
SetConnection(nullptr);
}
@@ -260,5 +260,7 @@ namespace AzToolsFramework
AZStd::atomic_bool m_validConnection;
SourceControlState m_connectionState;
bool m_p4ApplicationDetected { false };
};
} // namespace AzToolsFramework
@@ -0,0 +1,91 @@
/*
* 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
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzCore/EBus/EBus.h>
#include <AzCore/Component/EntityId.h>
#include <AzQtComponents/Components/ToastNotificationConfiguration.h>
#include <QPoint>
#endif
namespace AzToolsFramework
{
typedef AZ::EntityId ToastId;
/**
* An EBus for receiving notifications when a user interacts with or dismisses
* a toast notification.
*/
class ToastNotifications
: public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ToastId;
virtual void OnToastInteraction() {}
virtual void OnToastDismissed() {}
};
using ToastNotificationBus = AZ::EBus<ToastNotifications>;
typedef AZ::u32 ToastRequestBusId;
/**
* An EBus used to hide or show toast notifications. Generally, these request are handled by a
* ToastNotificationsView that has been created with a specific ToastRequestBusId
* e.g. AZ_CRC("ExampleToastNotificationView")
*/
class ToastRequests
: public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ToastRequestBusId; // bus is addressed by CRC of the view name
/**
* Hide a toast notification widget.
*
* @param toastId The toast notification's ToastId
*/
virtual void HideToastNotification(const ToastId& toastId) = 0;
/**
* Show a toast notification with the specified toast configuration. When handled by a ToastNotificationsView,
* notifications are queued and presented to the user in sequence.
*
* @param toastConfiguration The toast configuration
* @return a ToastId
*/
virtual ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0;
/**
* Show a toast notification with the specified toast configuration at the current moust cursor location.
*
* @param toastConfiguration The toast configuration
* @return a ToastId
*/
virtual ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0;
/**
* Show a toast notification with the specified toast configuration at the specified location.
*
* @param screenPosition The screen position
* @param anchorPoint The anchorPoint for the toast notification widget
* @param toastConfiguration The toast configuration
* @return a ToastId
*/
virtual ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration&) = 0;
};
using ToastRequestBus = AZ::EBus<ToastRequests>;
}
@@ -0,0 +1,180 @@
/*
* 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 <AzToolsFramework/UI/Notifications/ToastNotificationsView.h>
#include <AzQtComponents/Components/ToastNotification.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/functional.h>
namespace AzToolsFramework
{
ToastNotificationsView::ToastNotificationsView(QWidget* parent, ToastRequestBusId busId)
: QWidget(parent)
{
ToastRequestBus::Handler::BusConnect(busId);
}
ToastNotificationsView::~ToastNotificationsView()
{
ToastRequestBus::Handler::BusDisconnect();
}
void ToastNotificationsView::OnHide()
{
QWidget::hide();
if (m_activeNotification.IsValid())
{
auto notificationIter = m_notifications.find(m_activeNotification);
if (notificationIter != m_notifications.end())
{
notificationIter->second->hide();
}
}
}
void ToastNotificationsView::UpdateToastPosition()
{
if (m_activeNotification.IsValid())
{
auto notificationIter = m_notifications.find(m_activeNotification);
if (notificationIter != m_notifications.end())
{
notificationIter->second->UpdatePosition(GetGlobalPoint(), m_anchorPoint);
}
}
}
void ToastNotificationsView::OnShow()
{
QWidget::show();
if (m_activeNotification.IsValid() || !m_queuedNotifications.empty())
{
DisplayQueuedNotification();
}
}
ToastId ToastNotificationsView::ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration)
{
ToastId toastId = CreateToastNotification(toastConfiguration);
m_queuedNotifications.emplace_back(toastId);
if (!m_activeNotification.IsValid())
{
DisplayQueuedNotification();
}
return toastId;
}
ToastId ToastNotificationsView::ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration)
{
ToastId toastId = CreateToastNotification(toastConfiguration);
m_notifications[toastId]->ShowToastAtCursor();
return toastId;
}
ToastId ToastNotificationsView::ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration& toastConfiguration)
{
ToastId toastId = CreateToastNotification(toastConfiguration);
m_notifications[toastId]->ShowToastAtPoint(screenPosition, anchorPoint);
return toastId;
}
void ToastNotificationsView::HideToastNotification(const ToastId& toastId)
{
auto notificationIter = m_notifications.find(toastId);
if (notificationIter != m_notifications.end())
{
auto queuedIter = AZStd::find(m_queuedNotifications.begin(), m_queuedNotifications.end(), toastId);
if (queuedIter != m_queuedNotifications.end())
{
m_queuedNotifications.erase(queuedIter);
}
notificationIter->second->reject();
}
}
ToastId ToastNotificationsView::CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration)
{
AzQtComponents::ToastNotification* notification = aznew AzQtComponents::ToastNotification(parentWidget(), toastConfiguration);
ToastId toastId = AZ::Entity::MakeId();
m_notifications[toastId] = notification;
QObject::connect(
m_notifications[toastId], &AzQtComponents::ToastNotification::ToastNotificationHidden,
[toastId]()
{
ToastNotificationBus::Event(toastId, &ToastNotificationBus::Events::OnToastDismissed);
});
QObject::connect(
m_notifications[toastId], &AzQtComponents::ToastNotification::ToastNotificationInteraction,
[toastId]()
{
ToastNotificationBus::Event(toastId, &ToastNotificationBus::Events::OnToastInteraction);
});
return toastId;
}
QPoint ToastNotificationsView::GetGlobalPoint()
{
QPoint relativePoint = m_offset;
AZ_Assert(parentWidget(), "ToastNotificationsView has invalid parent QWidget");
if (m_anchorPoint.x() == 1.0)
{
relativePoint.setX(parentWidget()->width() - m_offset.x());
}
if (m_anchorPoint.y() == 1.0)
{
relativePoint.setY(parentWidget()->height() - m_offset.y());
}
return parentWidget()->mapToGlobal(relativePoint);
}
void ToastNotificationsView::DisplayQueuedNotification()
{
AZ_Assert(parentWidget(), "ToastNotificationsView has invalid parent QWidget");
if (m_queuedNotifications.empty() || !parentWidget()->isVisible() || !isVisible())
{
return;
}
ToastId toastId = m_queuedNotifications.front();
m_queuedNotifications.erase(m_queuedNotifications.begin());
auto notificationIter = m_notifications.find(toastId);
if (notificationIter != m_notifications.end())
{
m_activeNotification = toastId;
notificationIter->second->ShowToastAtPoint(GetGlobalPoint(), m_anchorPoint);
QObject::connect(
notificationIter->second, &AzQtComponents::ToastNotification::ToastNotificationHidden,
[&]()
{
m_activeNotification.SetInvalid();
DisplayQueuedNotification();
}
);
}
// If we didn't actually show something, recurse to avoid things getting stuck in the queue.
if (!m_activeNotification.IsValid())
{
DisplayQueuedNotification();
}
}
}
@@ -0,0 +1,65 @@
/*
* 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
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QWidget>
#include <QPoint>
#include <QPointF>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzToolsFramework/UI/Notifications/ToastBus.h>
#endif
namespace AzQtComponents
{
class ToastNotification;
}
namespace AzToolsFramework
{
/**
* \brief A QWidget that displays and manages a queue of toast notifications.
*
* This view must be updated by its parent when the parent widget is show, hidden, moved
* or resized because toast notifications are displayed on top of the parent and are not part
* of the layout, so they must be manually moved.
*/
class ToastNotificationsView final
: public QWidget
, protected ToastRequestBus::Handler
{
Q_OBJECT
public:
ToastNotificationsView(QWidget* parent, ToastRequestBusId busId);
~ToastNotificationsView() override;
void HideToastNotification(const ToastId& toastId) override;
ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) override;
ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) override;
ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration&) override;
void OnHide();
void OnShow();
void UpdateToastPosition();
private:
ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration);
void DisplayQueuedNotification();
QPoint GetGlobalPoint();
ToastId m_activeNotification;
AZStd::unordered_map<ToastId, AzQtComponents::ToastNotification*> m_notifications;
AZStd::vector<ToastId> m_queuedNotifications;
QPoint m_offset = QPoint(10, 10);
QPointF m_anchorPoint = QPointF(1, 0);
};
} // AzToolsFramework
@@ -119,6 +119,12 @@ namespace AzToolsFramework
int EntityOutlinerListModel::rowCount(const QModelIndex& parent) const
{
// For QTreeView models, non-0 columns shouldn't have children
if (parent.isValid() && parent.column() != 0)
{
return 0;
}
auto parentId = GetEntityFromIndex(parent);
AZStd::size_t childCount = 0;
@@ -133,17 +139,13 @@ namespace AzToolsFramework
QModelIndex EntityOutlinerListModel::index(int row, int column, const QModelIndex& parent) const
{
// sanity check
if (!hasIndex(row, column, parent) || (parent.isValid() && parent.column() != 0) || (row < 0 || row >= rowCount(parent)))
{
return QModelIndex();
}
auto parentId = GetEntityFromIndex(parent);
// We have the row and column, so we just need the child ID to construct our index
AZ::EntityId childId;
EditorEntityInfoRequestBus::EventResult(childId, parentId, &EditorEntityInfoRequestBus::Events::GetChild, row);
return GetIndexFromEntity(childId, column);
AZ_Assert(childId.IsValid(), "No child found for parent");
return createIndex(row, column, static_cast<AZ::u64>(childId));
}
QVariant EntityOutlinerListModel::data(const QModelIndex& index, int role) const
@@ -517,13 +519,18 @@ namespace AzToolsFramework
{
AZ::EntityId parentId;
EditorEntityInfoRequestBus::EventResult(parentId, id, &EditorEntityInfoRequestBus::Events::GetParent);
return GetIndexFromEntity(parentId, index.column());
return GetIndexFromEntity(parentId, 0);
}
return QModelIndex();
}
Qt::ItemFlags EntityOutlinerListModel::flags(const QModelIndex& index) const
{
if (!index.isValid())
{
return Qt::ItemIsDropEnabled;
}
Qt::ItemFlags itemFlags = QAbstractItemModel::flags(index);
switch (index.column())
{
@@ -1208,6 +1215,10 @@ namespace AzToolsFramework
void EntityOutlinerListModel::ProcessEntityUpdates()
{
AZ_PROFILE_FUNCTION(Editor);
if (!m_entityChangeQueued)
{
return;
}
m_entityChangeQueued = false;
if (m_layoutResetQueued)
{
@@ -1236,31 +1247,14 @@ namespace AzToolsFramework
{
AZ_PROFILE_SCOPE(Editor, "EntityOutlinerListModel::ProcessEntityUpdates:ChangeQueue");
// its faster to just do a bulk data change than to carefully pick out indices
// so we'll just merge all ranges into a single range rather than try to make gaps
QModelIndex firstChangeIndex;
QModelIndex lastChangeIndex;
for (auto entityId : m_entityChangeQueue)
{
auto myIndex = GetIndexFromEntity(entityId, ColumnName);
if ((!firstChangeIndex.isValid())||(firstChangeIndex.row() > myIndex.row()))
if (entityId.IsValid())
{
firstChangeIndex = myIndex;
const QModelIndex beginIndex = GetIndexFromEntity(entityId, ColumnName);
const QModelIndex endIndex = createIndex(beginIndex.row(), VisibleColumnCount - 1, beginIndex.internalId());
emit dataChanged(beginIndex, endIndex);
}
if ((!lastChangeIndex.isValid())||(lastChangeIndex.row() < myIndex.row()))
{
// expand it to be the last column:
lastChangeIndex = myIndex;
}
}
if (firstChangeIndex.isValid())
{
// expand to cover all visible columns:
lastChangeIndex = createIndex(lastChangeIndex.row(), VisibleColumnCount - 1, lastChangeIndex.internalPointer());
emit dataChanged(firstChangeIndex, lastChangeIndex);
}
m_entityChangeQueue.clear();
@@ -1382,6 +1376,9 @@ namespace AzToolsFramework
m_isFilterDirty = true;
QueueAncestorUpdate(parentId);
emit EnableSelectionUpdates(true);
// Remove any pending updates for this removed entity.
m_entityChangeQueue.erase(childId);
}
void EntityOutlinerListModel::OnEntityInfoUpdatedOrderBegin(AZ::EntityId parentId, AZ::EntityId childId, AZ::u64 index)
@@ -145,6 +145,8 @@ namespace AzToolsFramework
void SetSortMode(EntityOutliner::DisplaySortMode sortMode) { m_sortMode = sortMode; }
void SetDropOperationInProgress(bool inProgress);
void ProcessEntityUpdates();
Q_SIGNALS:
void ExpandEntity(const AZ::EntityId& entityId, bool expand);
void SelectEntity(const AZ::EntityId& entityId, bool select);
@@ -178,7 +180,6 @@ namespace AzToolsFramework
void QueueEntityUpdate(AZ::EntityId entityId);
void QueueAncestorUpdate(AZ::EntityId entityId);
void QueueEntityToExpand(AZ::EntityId entityId, bool expand);
void ProcessEntityUpdates();
void ProcessEntityInfoResetEnd();
AZStd::unordered_set<AZ::EntityId> m_entitySelectQueue;
AZStd::unordered_set<AZ::EntityId> m_entityExpandQueue;
@@ -27,6 +27,7 @@
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
@@ -135,6 +136,10 @@ namespace AzToolsFramework
return;
}
// Initialize Editor functionality for the Prefab Focus Handler
auto prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
prefabFocusInterface->InitializeEditorInterfaces();
EditorContextMenuBus::Handler::BusConnect();
EditorEventsBus::Handler::BusConnect();
PrefabInstanceContainerNotificationBus::Handler::BusConnect();
@@ -250,7 +255,7 @@ namespace AzToolsFramework
if (s_prefabPublicInterface->IsInstanceContainerEntity(selectedEntity))
{
// Edit Prefab
if (prefabWipFeaturesEnabled && !s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity))
if (!s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity))
{
QAction* editAction = menu->addAction(QObject::tr("Edit Prefab"));
editAction->setToolTip(QObject::tr("Edit the prefab in focus mode."));
@@ -766,15 +771,8 @@ namespace AzToolsFramework
{
return false;
}
outPrefabAssetPath = product->GetRelativePath();
auto asset = AZ::Data::AssetManager::Instance().GetAsset(
product->GetAssetId(),
azrtti_typeid<AZ::Prefab::ProceduralPrefabAsset>(),
AZ::Data::AssetLoadBehavior::Default);
return asset.BlockUntilLoadComplete() != AZ::Data::AssetData::AssetStatus::Error;
return true;
}
void PrefabIntegrationManager::WarnUserOfError(AZStd::string_view title, AZStd::string_view message)
@@ -1161,25 +1159,14 @@ namespace AzToolsFramework
{
s_editorEntityUiInterface->RegisterEntity(entityId, m_prefabUiHandler.GetHandlerId());
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled)
{
// Register entity as a container
s_containerEntityInterface->RegisterEntityAsContainer(entityId);
}
// Register entity as a container
s_containerEntityInterface->RegisterEntityAsContainer(entityId);
}
}
void PrefabIntegrationManager::OnPrefabComponentDeactivate(AZ::EntityId entityId)
{
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled && !s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId))
if (!s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId))
{
// Unregister entity as a container
s_containerEntityInterface->UnregisterEntityAsContainer(entityId);
@@ -316,14 +316,7 @@ namespace AzToolsFramework
void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const
{
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled)
{
// Focus on this prefab
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
}
// Focus on this prefab
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
}
}
@@ -198,6 +198,8 @@ namespace AzToolsFramework
virtual float ManipulatorCircleBoundWidth() const = 0;
//! Returns if sticky select is enabled or not.
virtual bool StickySelectEnabled() const = 0;
//! Returns the default viewport camera position.
virtual AZ::Vector3 DefaultEditorCameraPosition() const = 0;
protected:
~ViewportSettingsRequests() = default;
@@ -300,6 +302,12 @@ namespace AzToolsFramework
using EditorViewportInputTimeNowRequestBus = AZ::EBus<EditorViewportInputTimeNowRequests>;
//! The style of cursor override.
enum class CursorStyleOverride
{
Forbidden
};
//! Viewport requests for managing the viewport cursor state.
class ViewportMouseCursorRequests
{
@@ -310,6 +318,10 @@ namespace AzToolsFramework
virtual void EndCursorCapture() = 0;
//! Is the mouse over the viewport.
virtual bool IsMouseOver() const = 0;
//! Set the cursor style override.
virtual void SetOverrideCursor(CursorStyleOverride cursorStyleOverride) = 0;
//! Clear the cursor style override.
virtual void ClearOverrideCursor() = 0;
protected:
~ViewportMouseCursorRequests() = default;
@@ -44,6 +44,13 @@ AZ_CVAR(
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Display the aggregate world bounds for a given entity (the union of all world component Aabbs)");
AZ_CVAR(
bool,
ed_useCursorLockIconInFocusMode,
false,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Use a lock icon when the cursor is over entities that cannot be interacted with");
namespace AzToolsFramework
{
@@ -115,6 +122,33 @@ namespace AzToolsFramework
}
}
CursorEntityIdQuery::CursorEntityIdQuery(AZ::EntityId entityId, AZ::EntityId rootEntityId)
: m_entityId(entityId)
, m_containerAncestorEntityId(rootEntityId)
{
}
AZ::EntityId CursorEntityIdQuery::EntityIdUnderCursor() const
{
return m_entityId;
}
AZ::EntityId CursorEntityIdQuery::ContainerAncestorEntityId() const
{
return m_containerAncestorEntityId;
}
bool CursorEntityIdQuery::HasContainerAncestorEntityId() const
{
if (m_entityId.IsValid())
{
return m_entityId != m_containerAncestorEntityId;
}
return false;
}
EditorHelpers::EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache)
{
@@ -131,7 +165,7 @@ namespace AzToolsFramework
m_invalidClicks = AZStd::make_unique<InvalidClicks>(AZStd::move(invalidClicks));
}
AZ::EntityId EditorHelpers::HandleMouseInteraction(
CursorEntityIdQuery EditorHelpers::FindEntityIdUnderCursor(
const AzFramework::CameraState& cameraState, const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
@@ -195,6 +229,13 @@ namespace AzToolsFramework
// verify if the entity Id corresponds to an entity that is focused; if not, halt selection.
if (entityIdUnderCursor.IsValid() && !IsSelectableAccordingToFocusMode(entityIdUnderCursor))
{
if (ed_useCursorLockIconInFocusMode)
{
ViewportInteraction::ViewportMouseCursorRequestBus::Event(
viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor,
ViewportInteraction::CursorStyleOverride::Forbidden);
}
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down ||
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick)
@@ -202,17 +243,21 @@ namespace AzToolsFramework
m_invalidClicks->AddInvalidClick(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
}
return AZ::EntityId();
return CursorEntityIdQuery(AZ::EntityId(), AZ::EntityId());
}
ViewportInteraction::ViewportMouseCursorRequestBus::Event(
viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::ClearOverrideCursor);
// container entity support - if the entity that is being selected is part of a closed container,
// change the selection to the container instead.
if (ContainerEntityInterface* containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get())
{
return containerEntityInterface->FindHighestSelectableEntity(entityIdUnderCursor);
const auto highestSelectableEntity = containerEntityInterface->FindHighestSelectableEntity(entityIdUnderCursor);
return CursorEntityIdQuery(entityIdUnderCursor, highestSelectableEntity);
}
return entityIdUnderCursor;
return CursorEntityIdQuery(entityIdUnderCursor, AZ::EntityId());
}
void EditorHelpers::Display2d(
@@ -32,6 +32,28 @@ namespace AzToolsFramework
struct MouseInteractionEvent;
}
//!< Represents the result of a query to find the id of the entity under the cursor (if any).
class CursorEntityIdQuery
{
public:
CursorEntityIdQuery(AZ::EntityId entityId, AZ::EntityId rootEntityId);
//! Returns the entity id under the cursor (if any).
//! @note In the case of no entity id under the cursor, an invalid entity id is returned.
AZ::EntityId EntityIdUnderCursor() const;
//! Returns the topmost container entity id in the hierarchy if the entity id under the cursor is inside a container entity, otherwise returns the entity id.
//! @note In the case of no entity id under the cursor, an invalid entity id is returned.
AZ::EntityId ContainerAncestorEntityId() const;
//! Returns true if the query has a container ancestor entity id, otherwise false.
bool HasContainerAncestorEntityId() const;
private:
AZ::EntityId m_entityId; //<! The entity id under the cursor.
AZ::EntityId m_containerAncestorEntityId; //<! For entities in container entities, the topmost container entity id in the hierarchy, otherwise the entity id under the cursor.
};
//! EditorHelpers are the visualizations that appear for entities
//! when 'Display Helpers' is toggled on inside the editor.
//! These include but are not limited to entity icons and shape visualizations.
@@ -47,9 +69,9 @@ namespace AzToolsFramework
EditorHelpers& operator=(const EditorHelpers&) = delete;
~EditorHelpers() = default;
//! Handle any mouse interaction with the EditorHelpers.
//! Finds the id of the entity under the cursor (if any). For entities in container entities, also finds the topmost container entity id in the hierarchy.
//! Used to check if a particular entity was selected.
AZ::EntityId HandleMouseInteraction(
CursorEntityIdQuery FindEntityIdUnderCursor(
const AzFramework::CameraState& cameraState, const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
//! Do the drawing responsible for the EditorHelpers.
@@ -80,7 +80,7 @@ namespace AzToolsFramework
const AzFramework::CameraState cameraState = GetCameraState(viewportId);
m_cachedEntityIdUnderCursor = m_editorHelpers->HandleMouseInteraction(cameraState, mouseInteraction);
m_cachedEntityIdUnderCursor = m_editorHelpers->FindEntityIdUnderCursor(cameraState, mouseInteraction).ContainerAncestorEntityId();
// when left clicking, if we successfully clicked an entity, assign that
// to the entity field selected in the entity inspector (RPE)
@@ -27,6 +27,7 @@
#include <AzToolsFramework/Manipulators/ScaleManipulators.h>
#include <AzToolsFramework/Manipulators/TranslationManipulators.h>
#include <AzToolsFramework/Maths/TransformUtils.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
#include <AzToolsFramework/ToolsComponents/EditorVisibilityBus.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
@@ -1799,7 +1800,8 @@ namespace AzToolsFramework
const AzFramework::ViewportId viewportId = mouseInteraction.m_mouseInteraction.m_interactionId.m_viewportId;
const AzFramework::CameraState cameraState = GetCameraState(viewportId);
m_cachedEntityIdUnderCursor = m_editorHelpers->HandleMouseInteraction(cameraState, mouseInteraction);
const auto cursorEntityIdQuery = m_editorHelpers->FindEntityIdUnderCursor(cameraState, mouseInteraction);
m_cachedEntityIdUnderCursor = cursorEntityIdQuery.ContainerAncestorEntityId();
const auto selectClickEvent = ClickDetectorEventFromViewportInteraction(mouseInteraction);
m_cursorState.SetCurrentPosition(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
@@ -1825,8 +1827,6 @@ namespace AzToolsFramework
}
}
const AZ::EntityId entityIdUnderCursor = m_cachedEntityIdUnderCursor;
EditorContextMenuUpdate(m_contextMenu, mouseInteraction);
m_boxSelect.HandleMouseInteraction(mouseInteraction);
@@ -1842,6 +1842,21 @@ namespace AzToolsFramework
return true;
}
const AZ::EntityId entityIdUnderCursor = m_cachedEntityIdUnderCursor;
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick &&
mouseInteraction.m_mouseInteraction.m_mouseButtons.Left())
{
if (cursorEntityIdQuery.HasContainerAncestorEntityId())
{
if (auto prefabFocusInterface = AZ::Interface<Prefab::PrefabFocusInterface>::Get())
{
prefabFocusInterface->FocusOnPrefabInstanceOwningEntityId(cursorEntityIdQuery.ContainerAncestorEntityId());
return false;
}
}
}
bool stickySelect = false;
ViewportInteraction::ViewportSettingsRequestBus::EventResult(
stickySelect, viewportId, &ViewportInteraction::ViewportSettingsRequestBus::Events::StickySelectEnabled);
@@ -759,6 +759,9 @@ set(FILES
UI/Prefab/PrefabUiHandler.cpp
UI/Prefab/PrefabViewportFocusPathHandler.h
UI/Prefab/PrefabViewportFocusPathHandler.cpp
UI/Notifications/ToastNotificationsView.cpp
UI/Notifications/ToastNotificationsView.h
UI/Notifications/ToastBus.h
PythonTerminal/ScriptHelpDialog.cpp
PythonTerminal/ScriptHelpDialog.h
PythonTerminal/ScriptHelpDialog.ui
@@ -14,6 +14,7 @@
#include <Prefab/PrefabTestFixture.h>
#include <Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
namespace UnitTest
{
@@ -132,4 +133,32 @@ namespace UnitTest
EXPECT_TRUE(outputValue.HasMember("member"));
EXPECT_STREQ(outputValue.FindMember("member")->value.GetString(), "value");
}
TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_DefaultsToNotProcPrefab)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
dom.AddMember("Source", "foo.prefab", dom.GetAllocator());
AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom));
EXPECT_FALSE(fooTemplate.IsProcedural());
}
TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_DomDrivesFlagToTrue)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
dom.AddMember("Source", "foo.procprefab", dom.GetAllocator());
AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom));
EXPECT_TRUE(fooTemplate.IsProcedural());
// the second time should use the cached version of the flag
EXPECT_TRUE(fooTemplate.IsProcedural());
}
TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_FailsWithNoSource)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom));
EXPECT_FALSE(fooTemplate.IsProcedural());
}
}
@@ -0,0 +1,214 @@
/*
* 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 <AzCore/Serialization/SerializeContext.h>
#include <AzTest/AzTest.h>
#include <AzFramework/Entity/EntityContextBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Entity/EditorEntityContextComponent.h>
#include <AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <AzToolsFramework/UI/Outliner/EntityOutlinerListModel.hxx>
#include <AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h>
#include <AzToolsFramework/Undo/UndoSystem.h>
#include <Prefab/PrefabTestFixture.h>
#include <QAbstractItemModelTester>
namespace UnitTest
{
// Test fixture for the entity outliner model that uses a QAbstractItemModelTester to validate the state of the model
// when QAbstractItemModel signals fire. Tests will exit with a fatal error if an invalid state is detected.
class EntityOutlinerTest : public PrefabTestFixture
{
protected:
void SetUpEditorFixtureImpl() override
{
PrefabTestFixture::SetUpEditorFixtureImpl();
GetApplication()->RegisterComponentDescriptor(AzToolsFramework::EditorEntityContextComponent::CreateDescriptor());
m_model = AZStd::make_unique<AzToolsFramework::EntityOutlinerListModel>();
m_model->Initialize();
m_modelTester =
AZStd::make_unique<QAbstractItemModelTester>(m_model.get(), QAbstractItemModelTester::FailureReportingMode::Fatal);
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
m_undoStack, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetUndoStack);
AZ_Assert(m_undoStack, "Failed to look up undo stack from tools application");
// Create a new root prefab - the synthetic "NewLevel.prefab" that comes in by default isn't suitable for outliner tests
// because it's created before the EditorEntityModel that our EntityOutlinerListModel subscribes to, and we want to
// recreate it as part of the fixture regardless.
auto entityOwnershipService = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
entityOwnershipService->CreateNewLevelPrefab("UnitTestRoot.prefab", "");
}
void TearDownEditorFixtureImpl() override
{
m_undoStack = nullptr;
m_modelTester.reset();
m_model.reset();
PrefabTestFixture::TearDownEditorFixtureImpl();
}
// Creates an entity with a given name as one undoable operation
// Parents to parentId, or the root prefab container entity if parentId is invalid
AZ::EntityId CreateNamedEntity(AZStd::string name, AZ::EntityId parentId = AZ::EntityId())
{
auto createResult = m_prefabPublicInterface->CreateEntity(parentId, AZ::Vector3());
AZ_Assert(createResult.IsSuccess(), "Failed to create entity: %s", createResult.GetError().c_str());
AZ::EntityId entityId = createResult.GetValue();
AZ::Entity* entity = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
entity->Deactivate();
entity->SetName(name);
// Normally, in invalid parent ID should automatically parent us to the root prefab, but currently in the unit test
// environment entities aren't created with a default transform component, so CreateEntity won't correctly parent.
// We get the actual target parent ID here, then create our missing transform component.
if (!parentId.IsValid())
{
auto prefabEditorEntityOwnershipInterface = AZ::Interface<AzToolsFramework::PrefabEditorEntityOwnershipInterface>::Get();
parentId = prefabEditorEntityOwnershipInterface->GetRootPrefabInstance()->get().GetContainerEntityId();
}
auto transform = aznew AzToolsFramework::Components::TransformComponent;
transform->SetParent(parentId);
entity->AddComponent(transform);
entity->Activate();
// Update our undo cache entry to include the rename / reparent as one atomic operation.
m_prefabPublicInterface->GenerateUndoNodesForEntityChangeAndUpdateCache(entityId, m_undoStack->GetTop());
ProcessDeferredUpdates();
return entityId;
}
// Helper to visualize debug state
void PrintModel()
{
AZStd::deque<AZStd::pair<QModelIndex, int>> indices;
indices.push_back({ m_model->index(0, 0), 0 });
while (!indices.empty())
{
auto [index, depth] = indices.front();
indices.pop_front();
QString indentString;
for (int i = 0; i < depth; ++i)
{
indentString += " ";
}
qDebug() << (indentString + index.data(Qt::DisplayRole).toString()) << index.internalId();
for (int i = 0; i < m_model->rowCount(index); ++i)
{
indices.emplace_back(m_model->index(i, 0, index), depth + 1);
}
}
};
// Gets the index of the root prefab, i.e. the "New Level" container entity
QModelIndex GetRootIndex() const
{
return m_model->index(0, 0);
}
// Kicks off any updates scheduled for the next tick
void ProcessDeferredUpdates()
{
// Force a prefab propagation for updates that are deferred to the next tick.
m_prefabSystemComponent->OnSystemTick();
// Ensure the model process its entity update queue
m_model->ProcessEntityUpdates();
}
// Performs an undo operation and ensures the tick-scheduled updates happen
void Undo()
{
m_undoStack->Undo();
ProcessDeferredUpdates();
}
// Performs a redo operation and ensures the tick-scheduled updates happen
void Redo()
{
m_undoStack->Redo();
ProcessDeferredUpdates();
}
AZStd::unique_ptr<AzToolsFramework::EntityOutlinerListModel> m_model;
AZStd::unique_ptr<QAbstractItemModelTester> m_modelTester;
AzToolsFramework::UndoSystem::UndoStack* m_undoStack = nullptr;
};
TEST_F(EntityOutlinerTest, TestCreateFlatHierarchyUndoAndRedoWorks)
{
constexpr size_t entityCount = 10;
for (size_t i = 0; i < entityCount; ++i)
{
CreateNamedEntity(AZStd::string::format("Entity%zu", i));
EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1);
}
for (int i = entityCount; i > 0; --i)
{
Undo();
EXPECT_EQ(m_model->rowCount(GetRootIndex()), i - 1);
}
for (size_t i = 0; i < entityCount; ++i)
{
Redo();
EXPECT_EQ(m_model->rowCount(GetRootIndex()), i + 1);
}
}
TEST_F(EntityOutlinerTest, TestCreateNestedHierarchyUndoAndRedoWorks)
{
constexpr size_t depth = 5;
auto modelDepth = [this]() -> int
{
int depth = 0;
QModelIndex index = GetRootIndex();
while (m_model->rowCount(index) > 0)
{
++depth;
index = m_model->index(0, 0, index);
}
return depth;
};
AZ::EntityId parentId;
for (int i = 0; i < depth; i++)
{
parentId = CreateNamedEntity(AZStd::string::format("EntityDepth%i", i), parentId);
EXPECT_EQ(modelDepth(), i + 1);
}
for (int i = depth - 1; i >= 0; --i)
{
Undo();
EXPECT_EQ(modelDepth(), i);
}
for (int i = 0; i < depth; ++i)
{
Redo();
EXPECT_EQ(modelDepth(), i + 1);
}
}
} // namespace UnitTest
@@ -120,6 +120,8 @@ set(FILES
ToolsComponents/EditorLayerComponentTests.cpp
ToolsComponents/EditorTransformComponentTests.cpp
TransformComponent.cpp
UI/EntityIdQLineEditTests.cpp
UI/EntityOutlinerTests.cpp
UI/EntityPropertyEditorTests.cpp
UndoStack.cpp
Viewport/ClusterTests.cpp
@@ -86,7 +86,7 @@ bool CLevelInfo::ReadInfo()
usePrefabSystemForLevels, &AzFramework::ApplicationRequests::IsPrefabSystemForLevelsEnabled);
// Set up a default game type for legacy code.
m_defaultGameTypeName = "Mission0";
m_defaultGameTypeName = "mission0";
if (usePrefabSystemForLevels)
{
@@ -96,17 +96,17 @@ bool CLevelInfo::ReadInfo()
AZStd::string levelPath(m_levelPath);
AZStd::string xmlFile(levelPath);
xmlFile += "/LevelInfo.xml";
xmlFile += "/levelinfo.xml";
XmlNodeRef rootNode = GetISystem()->LoadXmlFromFile(xmlFile.c_str());
if (rootNode)
{
AZStd::string dataFile(levelPath);
dataFile += "/LevelDataAction.xml";
dataFile += "/leveldataaction.xml";
XmlNodeRef dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str());
if (!dataNode)
{
dataFile = levelPath + "/LevelData.xml";
dataFile = levelPath + "/leveldata.xml";
dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str());
}
@@ -614,7 +614,7 @@ ILevel* CLevelSystem::LoadLevelInternal(const char* _levelName)
}
{
AZStd::string missionXml("Mission_");
AZStd::string missionXml("mission_");
missionXml += pLevelInfo->m_defaultGameTypeName;
missionXml += ".xml";
AZStd::string xmlFile(pLevelInfo->GetPath());
@@ -72,7 +72,7 @@ struct FolderRootWatch::PlatformImplementation
// Add the folder to watch and track it
int watchHandle = inotify_add_watch(m_iNotifyHandle,
cleanPath.toUtf8().constData(),
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
{
@@ -95,7 +95,7 @@ struct FolderRootWatch::PlatformImplementation
int watchHandle = inotify_add_watch(m_iNotifyHandle,
dirName.toUtf8().constData(),
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
{
@@ -3573,19 +3573,61 @@ namespace AssetProcessor
QString knownPathBeforeWildcard = encodedFileData.left(slashBeforeWildcardIndex + 1); // include the slash
QString relativeSearch = encodedFileData.mid(slashBeforeWildcardIndex + 1); // skip the slash
for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i)
// Absolute path, just check the 1 scan folder
if (AZ::IO::PathView(encodedFileData.toUtf8().constData()).IsAbsolute())
{
const ScanFolderInfo* scanFolderInfo = &m_platformConfig->GetScanFolderAt(i);
if (!scanFolderInfo->RecurseSubFolders() && encodedFileData.contains("/"))
QString scanFolderName;
if (!m_platformConfig->ConvertToRelativePath(encodedFileData, resultDatabaseSourceName, scanFolderName))
{
continue;
AZ_Warning(
AssetProcessor::ConsoleChannel, false,
"'%s' does not appear to be in any input folder. Use relative paths instead.",
sourceDependency.m_sourceFileDependencyPath.c_str());
}
QDir rooted(scanFolderInfo->ScanPath());
QString absolutePath = rooted.absoluteFilePath(knownPathBeforeWildcard);
auto scanFolderInfo = m_platformConfig->GetScanFolderByPath(scanFolderName);
resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(absolutePath, relativeSearch, false, scanFolderInfo->RecurseSubFolders()));
// Make an absolute path that is ScanFolderPath + Part of search path before the wildcard
QDir rooted(scanFolderName);
QString scanFolderAndKnownSubPath = rooted.absoluteFilePath(knownPathBeforeWildcard);
resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(
scanFolderAndKnownSubPath, relativeSearch, false, scanFolderInfo->RecurseSubFolders()));
}
else // Relative path, check every scan folder
{
for (int i = 0; i < m_platformConfig->GetScanFolderCount(); ++i)
{
const ScanFolderInfo* scanFolderInfo = &m_platformConfig->GetScanFolderAt(i);
if (!scanFolderInfo->RecurseSubFolders() && encodedFileData.contains("/"))
{
continue;
}
QDir rooted(scanFolderInfo->ScanPath());
QString absolutePath = rooted.absoluteFilePath(knownPathBeforeWildcard);
resolvedDependencyList.append(m_platformConfig->FindWildcardMatches(
absolutePath, relativeSearch, false, scanFolderInfo->RecurseSubFolders()));
}
}
// Convert to relative paths
for (auto dependencyItr = resolvedDependencyList.begin(); dependencyItr != resolvedDependencyList.end();)
{
QString relativePath, scanFolder;
if (m_platformConfig->ConvertToRelativePath(*dependencyItr, relativePath, scanFolder))
{
*dependencyItr = relativePath;
++dependencyItr;
}
else
{
AZ_Warning("AssetProcessor", false, "Failed to get relative path for wildcard dependency file %s. Is the file within a scan folder?",
dependencyItr->toUtf8().constData());
dependencyItr = resolvedDependencyList.erase(dependencyItr);
}
}
resultDatabaseSourceName = encodedFileData.replace('\\', '/');
@@ -10,7 +10,9 @@
#include <utilities/BatchApplicationManager.h>
#include <utilities/ApplicationServer.h>
#include <AzFramework/Asset/AssetSystemComponent.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/Utils/Utils.h>
#include <connection/connectionManager.h>
#include <QCoreApplication>
#include <QTemporaryDir>
@@ -98,10 +100,26 @@ namespace AssetProcessorMessagesTests
int argC = 0;
m_batchApplicationManager = AZStd::make_unique<UnitTestBatchApplicationManager>(&argC, nullptr, nullptr);
m_batchApplicationManager->BeforeRun();
// Override Game Name to be "AutomatedTesting"
AssetUtilities::ComputeProjectName("AutomatedTesting", true);
auto registry = AZ::SettingsRegistry::Get();
EXPECT_NE(registry, nullptr);
constexpr AZ::SettingsRegistryInterface::FixedValueString bootstrapKey{
AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey
};
constexpr AZ::SettingsRegistryInterface::FixedValueString projectPathKey{ bootstrapKey + "/project_path" };
registry->Set(projectPathKey, "AutomatedTesting");
AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(*registry);
// Force the branch token into settings registry before starting the application manager.
// This avoids writing the asset_processor.setreg file which can cause fileIO errors.
const AZ::IO::FixedMaxPathString enginePath = AZ::Utils::GetEnginePath();
constexpr AZ::SettingsRegistryInterface::FixedValueString branchTokenKey{ bootstrapKey + "/assetProcessor_branch_token" };
AZStd::string token;
AZ::StringFunc::AssetPath::CalculateBranchToken(enginePath.c_str(), token);
registry->Set(branchTokenKey, token.c_str());
auto status = m_batchApplicationManager->BeforeRun();
ASSERT_EQ(status, ApplicationManager::BeforeRunStatus::Status_Success);
m_batchApplicationManager->m_platformConfiguration = new PlatformConfiguration();
m_batchApplicationManager->InitAssetProcessorManager();
@@ -159,21 +177,25 @@ namespace AssetProcessorMessagesTests
ASSERT_TRUE(result);
});
}
void TearDown() override
{
QEventLoop eventLoop;
if (m_batchApplicationManager->m_connectionManager)
{
QEventLoop eventLoop;
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit);
QObject::connect(m_batchApplicationManager->m_connectionManager, &ConnectionManager::ReadyToQuit, &eventLoop, &QEventLoop::quit);
m_batchApplicationManager->m_connectionManager->QuitRequested();
m_batchApplicationManager->m_connectionManager->QuitRequested();
eventLoop.exec();
eventLoop.exec();
}
m_assetSystemComponent->Deactivate();
if (m_assetSystemComponent)
{
m_assetSystemComponent->Deactivate();
}
m_batchApplicationManager->Destroy();
}
@@ -505,6 +505,14 @@ bool ApplicationManager::StartAZFramework()
AzFramework::Application::Descriptor appDescriptor;
AZ::ComponentApplication::StartupParameters params;
QDir projectPath{ AssetUtilities::ComputeProjectPath() };
if (!projectPath.exists("project.json"))
{
AZStd::string errorMsg = AZStd::string::format("Path '%s' is not a valid project path.", projectPath.path().toUtf8().constData());
AssetProcessor::MessageInfoBus::Broadcast(&AssetProcessor::MessageInfoBus::Events::OnErrorMessage, errorMsg.c_str());
return false;
}
QString projectName = AssetUtilities::ComputeProjectName();
// Prevent loading of gems in the Create method of the ComponentApplication
@@ -520,7 +528,6 @@ bool ApplicationManager::StartAZFramework()
//Registering all the Components
m_frameworkApp.RegisterComponentDescriptor(AzFramework::LogComponent::CreateDescriptor());
Reflect();
const AzFramework::CommandLine* commandLine = nullptr;
@@ -95,6 +95,8 @@ GUIApplicationManager::~GUIApplicationManager()
ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun()
{
AssetProcessor::MessageInfoBus::Handler::BusConnect();
ApplicationManager::BeforeRunStatus status = ApplicationManagerBase::BeforeRun();
if (status != ApplicationManager::BeforeRunStatus::Status_Success)
{
@@ -109,7 +111,6 @@ ApplicationManager::BeforeRunStatus GUIApplicationManager::BeforeRun()
#if defined(EXTERNAL_CRASH_REPORTING)
CrashHandler::ToolsCrashHandler::InitCrashHandler("AssetProcessor", projectAssetRoot.absolutePath().toStdString());
#endif
AssetProcessor::MessageInfoBus::Handler::BusConnect();
// we have to monitor both the cache folder and the database file and restart AP if either of them gets deleted
// It is important to note that we are monitoring the parent folder and not the actual cache folder itself since
@@ -436,98 +437,7 @@ bool GUIApplicationManager::OnError(const char* /*window*/, const char* message)
connection = Qt::QueuedConnection;
}
if (m_isCurrentlyLoadingGems)
{
// if something goes wrong during gem initialization, this is a special case and we need to be extra helpful.
const char* userSettingsFile = "_WAF_/user_settings.options";
const char* defaultSettingsFile = "_WAF_/default_settings.json";
QDir engineRoot;
AssetUtilities::ComputeEngineRoot(engineRoot);
QString settingsPath = engineRoot.absoluteFilePath(userSettingsFile);
QString friendlyErrorMessage;
bool usingDefaults = false;
if (QFile::exists(settingsPath))
{
QSettings loader(settingsPath, QSettings::IniFormat);
QVariant settingValue = loader.value("Game Projects/enabled_game_projects");
QStringList compiledProjects = settingValue.toStringList();
if (compiledProjects.isEmpty())
{
QByteArray byteArray;
QFile jsonFile;
jsonFile.setFileName(engineRoot.absoluteFilePath(defaultSettingsFile));
jsonFile.open(QIODevice::ReadOnly | QIODevice::Text);
byteArray = jsonFile.readAll();
jsonFile.close();
QJsonObject settingsObject = QJsonDocument::fromJson(byteArray).object();
QJsonArray projectsArray = settingsObject["Game Projects"].toArray();
if (!projectsArray.isEmpty())
{
auto projectObject = projectsArray[0].toObject();
QString projects = projectObject["default_value"].toString();
if (!projects.isEmpty())
{
compiledProjects = projects.split(',');
usingDefaults = true;
}
}
}
for (int i = 0; i < compiledProjects.size(); ++i)
{
compiledProjects[i] = compiledProjects[i].trimmed();
}
QString enabledProject = AssetUtilities::ComputeProjectName();
if (!compiledProjects.contains(enabledProject))
{
QString projectSourceLine;
if (usingDefaults)
{
projectSourceLine = QString("The currently compiled projects according to the defaults in %1 are '%2'").arg(defaultSettingsFile);
}
else
{
projectSourceLine = QString("The currently compiled projects according to %1 are '%2'").arg(userSettingsFile);
}
projectSourceLine = projectSourceLine.arg(compiledProjects.join(", "));
friendlyErrorMessage = QString("An error occurred while loading gems.\n"
"The enabled game project is not in the list of compiled projects.\n"
"Please configure the enabled project to be compiled and rebuild or change the enabled project.\n"
"The currently enabled game project (from bootstrap.cfg or /%4 command-line parameter) is '%1'.\n"
"%2\n"
"Full error text:\n"
"%3"
).arg(enabledProject).arg(projectSourceLine).arg(message).arg(AssetUtilities::ProjectPathOverrideParameter);
}
}
if (friendlyErrorMessage.isEmpty())
{
friendlyErrorMessage = QString("An error occurred while loading gems.\n"
"This can happen when new gems are added to a project, but those gems need to be built in order to function.\n"
"This can also happen when switching to a different project, one which uses gems which are not yet built.\n"
"To continue, please build the current project before attempting to run Asset Processor again.\n\n"
"Full error text:\n"
"%1").arg(message);
}
QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, friendlyErrorMessage), Q_ARG(bool, true));
}
else
{
QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true));
}
QMetaObject::invokeMethod(this, "ShowMessageBox", connection, Q_ARG(QString, QString("Error")), Q_ARG(QString, QString(message)), Q_ARG(bool, true));
return true;
}
@@ -1435,32 +1435,35 @@ namespace AssetProcessor
return QString();
}
QStringList PlatformConfiguration::FindWildcardMatches(const QString& sourceFolder, QString relativeName, bool includeFolders, bool recursiveSearch) const
QStringList PlatformConfiguration::FindWildcardMatches(
const QString& sourceFolder, QString relativeName, bool includeFolders, bool recursiveSearch) const
{
if (relativeName.isEmpty())
{
return QStringList();
}
const int pathLen = sourceFolder.length() + 1;
QDir sourceFolderDir(sourceFolder);
relativeName.replace('\\', '/');
QString posixRelativeName = QDir::fromNativeSeparators(relativeName);
QStringList returnList;
QRegExp nameMatch{ relativeName, Qt::CaseInsensitive, QRegExp::Wildcard };
QDirIterator diretoryIterator(sourceFolder, QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot, recursiveSearch ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
QRegExp nameMatch{ posixRelativeName, Qt::CaseInsensitive, QRegExp::Wildcard };
QDirIterator dirIterator(
sourceFolderDir.path(), QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot,
recursiveSearch ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
QStringList files;
while (diretoryIterator.hasNext())
while (dirIterator.hasNext())
{
diretoryIterator.next();
if (!includeFolders && !diretoryIterator.fileInfo().isFile())
dirIterator.next();
if (!includeFolders && !dirIterator.fileInfo().isFile())
{
continue;
}
QString pathMatch{ diretoryIterator.filePath().mid(pathLen) };
QString pathMatch{ sourceFolderDir.relativeFilePath(dirIterator.filePath()) };
if (nameMatch.exactMatch(pathMatch))
{
returnList.append(AssetUtilities::NormalizeFilePath(diretoryIterator.filePath()));
returnList.append(QDir::fromNativeSeparators(dirIterator.filePath()));
}
}
return returnList;
@@ -7,7 +7,7 @@
*/
#include <GemRepo/GemRepoAddDialog.h>
#include <FormLineEditWidget.h>
#include <FormFolderBrowseEditWidget.h>
#include <QVBoxLayout>
#include <QLabel>
@@ -40,7 +40,7 @@ namespace O3DE::ProjectManager
instructionContextLabel->setAlignment(Qt::AlignLeft);
vLayout->addWidget(instructionContextLabel);
m_repoPath = new FormLineEditWidget(tr("Repository Path"), "", this);
m_repoPath = new FormFolderBrowseEditWidget(tr("Repository Path"), "", this);
m_repoPath->setFixedWidth(600);
vLayout->addWidget(m_repoPath);
@@ -36,7 +36,7 @@ namespace O3DE::ProjectManager
QString m_summary = "No summary provided.";
QString m_additionalInfo = "";
QString m_directoryLink = "";
QString m_repoLink = "";
QString m_repoUri = "";
QStringList m_includedGemPaths = {};
QDateTime m_lastUpdated;
};
@@ -60,8 +60,8 @@ namespace O3DE::ProjectManager
// Repo name and url link
m_nameLabel->setText(m_model->GetName(modelIndex));
m_repoLinkLabel->setText(m_model->GetRepoLink(modelIndex));
m_repoLinkLabel->SetUrl(m_model->GetRepoLink(modelIndex));
m_repoLinkLabel->setText(m_model->GetRepoUri(modelIndex));
m_repoLinkLabel->SetUrl(m_model->GetRepoUri(modelIndex));
// Repo summary
m_summaryLabel->setText(m_model->GetSummary(modelIndex));
@@ -145,6 +145,11 @@ namespace O3DE::ProjectManager
GemRepoModel::SetEnabled(*model, modelIndex, !isAdded);
return true;
}
else if (keyEvent->key() == Qt::Key_X)
{
emit RemoveRepo(modelIndex);
return true;
}
}
if (event->type() == QEvent::MouseButtonPress)
@@ -154,6 +159,7 @@ namespace O3DE::ProjectManager
QRect fullRect, itemRect, contentRect;
CalcRects(option, fullRect, itemRect, contentRect);
const QRect buttonRect = CalcButtonRect(contentRect);
const QRect deleteButtonRect = CalcDeleteButtonRect(contentRect);
if (buttonRect.contains(mouseEvent->pos()))
{
@@ -161,6 +167,11 @@ namespace O3DE::ProjectManager
GemRepoModel::SetEnabled(*model, modelIndex, !isAdded);
return true;
}
else if (deleteButtonRect.contains(mouseEvent->pos()))
{
emit RemoveRepo(modelIndex);
return true;
}
}
return QStyledItemDelegate::editorEvent(event, model, option, modelIndex);
@@ -214,9 +225,14 @@ namespace O3DE::ProjectManager
painter->restore();
}
QRect GemRepoItemDelegate::CalcDeleteButtonRect(const QRect& contentRect) const
{
const QPoint topLeft = QPoint(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2);
return QRect(topLeft, QSize(s_iconSize, s_iconSize));
}
void GemRepoItemDelegate::DrawEditButtons(QPainter* painter, const QRect& contentRect) const
{
painter->drawPixmap(contentRect.right() - s_iconSize * 2 - s_iconSpacing, contentRect.center().y() - s_iconSize / 2, m_editIcon);
painter->drawPixmap(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2, m_deleteIcon);
}
@@ -66,10 +66,14 @@ namespace O3DE::ProjectManager
inline constexpr static int s_refreshIconSize = 14;
inline constexpr static int s_refreshIconSpacing = 10;
signals:
void RemoveRepo(const QModelIndex& modelIndex);
protected:
void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const;
QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const;
QRect CalcButtonRect(const QRect& contentRect) const;
QRect CalcDeleteButtonRect(const QRect& contentRect) const;
void DrawButton(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const;
void DrawEditButtons(QPainter* painter, const QRect& contentRect) const;
@@ -9,6 +9,8 @@
#include <GemRepo/GemRepoListView.h>
#include <GemRepo/GemRepoItemDelegate.h>
#include <QShortcut>
namespace O3DE::ProjectManager
{
GemRepoListView::GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent)
@@ -19,6 +21,9 @@ namespace O3DE::ProjectManager
setModel(model);
setSelectionModel(selectionModel);
setItemDelegate(new GemRepoItemDelegate(model, this));
GemRepoItemDelegate* itemDelegate = new GemRepoItemDelegate(model, this);
connect(itemDelegate, &GemRepoItemDelegate::RemoveRepo, this, &GemRepoListView::RemoveRepo);
setItemDelegate(itemDelegate);
}
} // namespace O3DE::ProjectManager
@@ -25,5 +25,8 @@ namespace O3DE::ProjectManager
public:
explicit GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr);
~GemRepoListView() = default;
signals:
void RemoveRepo(const QModelIndex& modelIndex);
};
} // namespace O3DE::ProjectManager
@@ -37,7 +37,7 @@ namespace O3DE::ProjectManager
item->setData(gemRepoInfo.m_summary, RoleSummary);
item->setData(gemRepoInfo.m_isEnabled, RoleIsEnabled);
item->setData(gemRepoInfo.m_directoryLink, RoleDirectoryLink);
item->setData(gemRepoInfo.m_repoLink, RoleRepoLink);
item->setData(gemRepoInfo.m_repoUri, RoleRepoUri);
item->setData(gemRepoInfo.m_lastUpdated, RoleLastUpdated);
item->setData(gemRepoInfo.m_path, RolePath);
item->setData(gemRepoInfo.m_additionalInfo, RoleAdditionalInfo);
@@ -83,9 +83,9 @@ namespace O3DE::ProjectManager
return modelIndex.data(RoleDirectoryLink).toString();
}
QString GemRepoModel::GetRepoLink(const QModelIndex& modelIndex)
QString GemRepoModel::GetRepoUri(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleRepoLink).toString();
return modelIndex.data(RoleRepoUri).toString();
}
QDateTime GemRepoModel::GetLastUpdated(const QModelIndex& modelIndex)
@@ -35,7 +35,7 @@ namespace O3DE::ProjectManager
static QString GetSummary(const QModelIndex& modelIndex);
static QString GetAdditionalInfo(const QModelIndex& modelIndex);
static QString GetDirectoryLink(const QModelIndex& modelIndex);
static QString GetRepoLink(const QModelIndex& modelIndex);
static QString GetRepoUri(const QModelIndex& modelIndex);
static QDateTime GetLastUpdated(const QModelIndex& modelIndex);
static QString GetPath(const QModelIndex& modelIndex);
@@ -55,7 +55,7 @@ namespace O3DE::ProjectManager
RoleSummary,
RoleIsEnabled,
RoleDirectoryLink,
RoleRepoLink,
RoleRepoUri,
RoleLastUpdated,
RolePath,
RoleAdditionalInfo,
@@ -25,6 +25,7 @@
#include <QTableWidget>
#include <QFrame>
#include <QStackedWidget>
#include <QMessageBox>
namespace O3DE::ProjectManager
{
@@ -79,21 +80,48 @@ namespace O3DE::ProjectManager
if (repoAddDialog->exec() == QDialog::DialogCode::Accepted)
{
QString repoUrl = repoAddDialog->GetRepoPath();
if (repoUrl.isEmpty())
QString repoUri = repoAddDialog->GetRepoPath();
if (repoUri.isEmpty())
{
QMessageBox::warning(this, tr("No Input"), tr("Please provide a repo Uri."));
return;
}
AZ::Outcome<void, AZStd::string> addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUrl);
if (addGemRepoResult.IsSuccess())
bool addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUri);
if (addGemRepoResult)
{
Reinit();
}
else
{
QMessageBox::critical(this, tr("Operation failed"),
QString("Failed to add gem repo: %1.<br>Error:<br>%2").arg(repoUrl, addGemRepoResult.GetError().c_str()));
QString failureMessage = tr("Failed to add gem repo: %1.").arg(repoUri);
QMessageBox::critical(this, tr("Operation failed"), failureMessage);
AZ_Error("Project Manger", false, failureMessage.toUtf8());
}
}
}
void GemRepoScreen::HandleRemoveRepoButton(const QModelIndex& modelIndex)
{
QString repoName = m_gemRepoModel->GetName(modelIndex);
QMessageBox::StandardButton warningResult = QMessageBox::warning(
this, tr("Remove Repo"), tr("Are you sure you would like to remove gem repo: %1?").arg(repoName),
QMessageBox::No | QMessageBox::Yes);
if (warningResult == QMessageBox::Yes)
{
QString repoUri = m_gemRepoModel->GetRepoUri(modelIndex);
bool removeGemRepoResult = PythonBindingsInterface::Get()->RemoveGemRepo(repoUri);
if (removeGemRepoResult)
{
Reinit();
}
else
{
QString failureMessage = tr("Failed to remove gem repo: %1.").arg(repoUri);
QMessageBox::critical(this, tr("Operation failed"), failureMessage);
AZ_Error("Project Manger", false, failureMessage.toUtf8());
}
}
}
@@ -251,6 +279,8 @@ namespace O3DE::ProjectManager
m_gemRepoListView = new GemRepoListView(m_gemRepoModel, m_gemRepoModel->GetSelectionModel(), this);
middleVLayout->addWidget(m_gemRepoListView);
connect(m_gemRepoListView, &GemRepoListView::RemoveRepo, this, &GemRepoScreen::HandleRemoveRepoButton);
hLayout->addLayout(middleVLayout);
m_gemRepoInspector = new GemRepoInspector(m_gemRepoModel, this);
@@ -39,6 +39,7 @@ namespace O3DE::ProjectManager
public slots:
void HandleAddRepoButton();
void HandleRemoveRepoButton(const QModelIndex& modelIndex);
private:
void FillModel();
@@ -939,17 +939,60 @@ namespace O3DE::ProjectManager
}
}
AZ::Outcome<void, AZStd::string> PythonBindings::AddGemRepo(const QString& repoUri)
bool PythonBindings::AddGemRepo(const QString& repoUri)
{
// o3de scripts need method added
(void)repoUri;
return AZ::Failure<AZStd::string>("Adding Gem Repo not implemented yet in o3de scripts.");
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
auto pyUri = QString_To_Py_String(repoUri);
auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pybind11::none(), pyUri);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
return result && registrationResult;
}
bool PythonBindings::RemoveGemRepo(const QString& repoUri)
{
bool registrationResult = false;
bool result = ExecuteWithLock(
[&]
{
auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), // engine_path
pybind11::none(), // project_path
pybind11::none(), // gem_path
pybind11::none(), // external_subdir_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
QString_To_Py_String(repoUri), // 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>();
});
return result && registrationResult;
}
GemRepoInfo PythonBindings::GetGemRepoInfo(pybind11::handle repoUri)
{
GemRepoInfo gemRepoInfo;
gemRepoInfo.m_repoLink = Py_To_String(repoUri);
gemRepoInfo.m_repoUri = Py_To_String(repoUri);
auto data = m_manifest.attr("get_repo_json_data")(repoUri);
if (pybind11::isinstance<pybind11::dict>(data))
@@ -957,7 +1000,7 @@ namespace O3DE::ProjectManager
try
{
// required
gemRepoInfo.m_repoLink = Py_To_String(data["repo_uri"]);
gemRepoInfo.m_repoUri = Py_To_String(data["repo_uri"]);
gemRepoInfo.m_name = Py_To_String(data["repo_name"]);
gemRepoInfo.m_creator = Py_To_String(data["origin"]);
@@ -1019,13 +1062,13 @@ namespace O3DE::ProjectManager
#else
GemRepoInfo mockJohnRepo("JohnCreates", "John Smith", QDateTime(QDate(2021, 8, 31), QTime(11, 57)), true);
mockJohnRepo.m_summary = "John's Summary. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sollicitudin dapibus urna";
mockJohnRepo.m_repoLink = "https://github.com/o3de/o3de";
mockJohnRepo.m_repoUri = "https://github.com/o3de/o3de";
mockJohnRepo.m_additionalInfo = "John's additional info. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sollicitu.";
gemRepos.push_back(mockJohnRepo);
GemRepoInfo mockJaneRepo("JanesGems", "Jane Doe", QDateTime(QDate(2021, 9, 10), QTime(18, 23)), false);
mockJaneRepo.m_summary = "Jane's Summary.";
mockJaneRepo.m_repoLink = "https://github.com/o3de/o3de.org";
mockJaneRepo.m_repoUri = "https://github.com/o3de/o3de.org";
gemRepos.push_back(mockJaneRepo);
#endif // MOCK_GEM_REPO_INFO
@@ -58,7 +58,8 @@ namespace O3DE::ProjectManager
AZ::Outcome<QVector<ProjectTemplateInfo>> GetProjectTemplates(const QString& projectPath = {}) override;
// Gem Repos
AZ::Outcome<void, AZStd::string> AddGemRepo(const QString& repoUri) override;
bool AddGemRepo(const QString& repoUri) override;
bool RemoveGemRepo(const QString& repoUri) override;
AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
private:
@@ -169,11 +169,18 @@ namespace O3DE::ProjectManager
// Gem Repos
/**
* A gem repo to engine. Registers this gem repo with the current engine.
* @param repoUri the absolute filesystem path or url to the gem repo manifest file.
* @return An outcome with the success flag as well as an error message in case of a failure.
* Registers this gem repo with the current engine.
* @param repoUri the absolute filesystem path or url to the gem repo.
* @return true on success, false on failure.
*/
virtual AZ::Outcome<void, AZStd::string> AddGemRepo(const QString& repoUri) = 0;
virtual bool AddGemRepo(const QString& repoUri) = 0;
/**
* Unregisters this gem repo with the current engine.
* @param repoUri the absolute filesystem path or url to the gem repo.
* @return true on success, false on failure.
*/
virtual bool RemoveGemRepo(const QString& repoUri) = 0;
/**
* Get all available gem repo infos. Gathers all repos registered with the engine.