Merge remote-tracking branch 'upstream/development' into Atom/rbarrand/MaterialVersionUpdate
This commit is contained in:
+18
-16
@@ -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
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
+71
-24
@@ -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++)
|
||||
{
|
||||
|
||||
+2
@@ -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.
|
||||
|
||||
+1
@@ -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>
|
||||
+18
@@ -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
|
||||
+46
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+18
-5
@@ -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>;
|
||||
}
|
||||
+180
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -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
|
||||
+26
-29
@@ -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)
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+10
-23
@@ -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;
|
||||
|
||||
+49
-4
@@ -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.
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+18
-3
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user