Merge remote-tracking branch 'upstream/development' into Atom/santorac/RemixableMaterialTypes3

Signed-off-by: santorac <55155825+santorac@users.noreply.github.com>
This commit is contained in:
santorac
2022-01-31 09:21:18 -08:00
261 changed files with 12222 additions and 2556 deletions
@@ -38,7 +38,7 @@ def Menus_FileMenuOptions_Work():
("Save As",),
("Save Level Statistics",),
("Edit Project Settings",),
#("Edit Platform Settings",), Temporarily disabled due to https://github.com/o3de/o3de/issues/6604
("Edit Platform Settings",),
("New Project",),
("Open Project",),
("Show Log File",),
-1
View File
@@ -13,7 +13,6 @@
#include <QMap>
#include <QTranslator>
#include <QSet>
#include "IEventLoopHook.h"
#include <unordered_map>
#include <AzCore/PlatformDef.h>
-28
View File
@@ -1828,34 +1828,6 @@ bool CCryEditApp::InitInstance()
return true;
}
void CCryEditApp::RegisterEventLoopHook(IEventLoopHook* pHook)
{
pHook->pNextHook = m_pEventLoopHook;
m_pEventLoopHook = pHook;
}
void CCryEditApp::UnregisterEventLoopHook(IEventLoopHook* pHookToRemove)
{
IEventLoopHook* pPrevious = nullptr;
for (IEventLoopHook* pHook = m_pEventLoopHook; pHook != nullptr; pHook = pHook->pNextHook)
{
if (pHook == pHookToRemove)
{
if (pPrevious)
{
pPrevious->pNextHook = pHookToRemove->pNextHook;
}
else
{
m_pEventLoopHook = pHookToRemove->pNextHook;
}
pHookToRemove->pNextHook = nullptr;
return;
}
}
}
//////////////////////////////////////////////////////////////////////////
void CCryEditApp::LoadFile(QString fileName)
{
-4
View File
@@ -30,7 +30,6 @@ class CConsoleDialog;
struct mg_connection;
struct mg_request_info;
struct mg_context;
struct IEventLoopHook;
class QAction;
class MainWindow;
class QSharedMemory;
@@ -153,8 +152,6 @@ public:
int IdleProcessing(bool bBackground);
bool IsWindowInForeground();
void RunInitPythonScript(CEditCommandLineInfo& cmdInfo);
void RegisterEventLoopHook(IEventLoopHook* pHook);
void UnregisterEventLoopHook(IEventLoopHook* pHook);
void DisableIdleProcessing() override;
void EnableIdleProcessing() override;
@@ -344,7 +341,6 @@ private:
QString m_lastOpenLevelPath;
CQuickAccessBar* m_pQuickAccessBar = nullptr;
IEventLoopHook* m_pEventLoopHook = nullptr;
QString m_rootEnginePath;
int m_disableIdleProcessingCounter = 0; //!< Counts requests to disable idle processing. When non-zero, idle processing will be disabled.
-6
View File
@@ -66,7 +66,6 @@ class IAWSResourceManager;
struct ISystem;
struct IRenderer;
struct AABB;
struct IEventLoopHook;
struct IErrorReport; // Vladimir@conffx
struct IFileUtil; // Vladimir@conffx
struct IEditorLog; // Vladimir@conffx
@@ -509,11 +508,6 @@ struct IEditor
virtual void SetActiveView(CViewport* viewport) = 0;
virtual struct IEditorFileMonitor* GetFileMonitor() = 0;
// These are needed for Qt integration:
virtual void RegisterEventLoopHook(IEventLoopHook* pHook) = 0;
virtual void UnregisterEventLoopHook(IEventLoopHook* pHook) = 0;
// ^^^
//! QMimeData is used by the Qt clipboard.
//! IMPORTANT: Any QMimeData allocated for the clipboard will be deleted
//! when the editor exists. If a QMimeData is allocated by a different
-10
View File
@@ -789,16 +789,6 @@ IEditorFileMonitor* CEditorImpl::GetFileMonitor()
return m_pEditorFileMonitor.get();
}
void CEditorImpl::RegisterEventLoopHook(IEventLoopHook* pHook)
{
CCryEditApp::instance()->RegisterEventLoopHook(pHook);
}
void CEditorImpl::UnregisterEventLoopHook(IEventLoopHook* pHook)
{
CCryEditApp::instance()->UnregisterEventLoopHook(pHook);
}
float CEditorImpl::GetTerrainElevation(float x, float y)
{
float terrainElevation = AzFramework::Terrain::TerrainDataRequests::GetDefaultTerrainHeight();
-2
View File
@@ -155,8 +155,6 @@ public:
CMusicManager* GetMusicManager() override { return m_pMusicManager; };
IEditorFileMonitor* GetFileMonitor() override;
void RegisterEventLoopHook(IEventLoopHook* pHook) override;
void UnregisterEventLoopHook(IEventLoopHook* pHook) override;
IIconManager* GetIconManager() override;
float GetTerrainElevation(float x, float y) override;
Editor::EditorQtApplication* GetEditorQtApplication() override { return m_QtApplication; }
@@ -135,9 +135,6 @@ struct IClassDesc
//////////////////////////////////////////////////////////////////////////
};
struct IViewPaneClass;
struct CRYEDIT_API IEditorClassFactory
{
public:
@@ -149,7 +146,6 @@ public:
virtual IClassDesc* FindClass(const char* pClassName) const = 0;
//! Find class in the factory by class id
virtual IClassDesc* FindClass(const GUID& rClassID) const = 0;
virtual IViewPaneClass* FindViewPaneClassByTitle(const char* pPaneTitle) const = 0;
virtual void UnregisterClass(const char* pClassName) = 0;
virtual void UnregisterClass(const GUID& rClassID) = 0;
//! Get classes that matching specific requirements.
-24
View File
@@ -1,24 +0,0 @@
/*
* 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
*
*/
#ifndef CRYINCLUDE_EDITOR_INCLUDE_IEVENTLOOPHOOK_H
#define CRYINCLUDE_EDITOR_INCLUDE_IEVENTLOOPHOOK_H
#pragma once
struct IEventLoopHook
{
IEventLoopHook* pNextHook;
IEventLoopHook()
: pNextHook(0) {}
virtual bool PrePumpMessage() { return false; }
};
#endif // CRYINCLUDE_EDITOR_INCLUDE_IEVENTLOOPHOOK_H
-74
View File
@@ -1,74 +0,0 @@
/*
* 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
*
*/
#ifndef CRYINCLUDE_EDITOR_INCLUDE_IVIEWPANE_H
#define CRYINCLUDE_EDITOR_INCLUDE_IVIEWPANE_H
#pragma once
#include "IEditorClassFactory.h"
#include <QSize>
class QWidget;
class QRect;
struct IViewPaneClass
: public IClassDesc
{
DEFINE_UUID(0x7E13EC7C, 0xF621, 0x4aeb, 0xB6, 0x42, 0x67, 0xD7, 0x8E, 0xD4, 0x68, 0xF8)
enum EDockingDirection
{
DOCK_TOP,
DOCK_LEFT,
DOCK_RIGHT,
DOCK_BOTTOM,
DOCK_FLOAT,
};
virtual ~IViewPaneClass() = default;
// Return text for view pane title.
virtual QString GetPaneTitle() = 0;
// Return the string resource ID for the title's text.
virtual unsigned int GetPaneTitleID() const = 0;
// Return preferable initial docking position for pane.
virtual EDockingDirection GetDockingDirection() = 0;
// Initial pane size.
virtual QRect GetPaneRect() = 0;
// Get Minimal view size
virtual QSize GetMinSize() { return QSize(0, 0); }
// Return true if only one pane at a time of time view class can be created.
virtual bool SinglePane() = 0;
// Return true if the view window wants to get ID_IDLE_UPDATE commands.
virtual bool WantIdleUpdate() = 0;
//////////////////////////////////////////////////////////////////////////
// IUnknown
//////////////////////////////////////////////////////////////////////////
HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObj)
{
if (riid == __az_uuidof(IViewPaneClass))
{
*ppvObj = this;
return S_OK;
}
return E_NOINTERFACE;
}
//////////////////////////////////////////////////////////////////////////
};
#endif // CRYINCLUDE_EDITOR_INCLUDE_IVIEWPANE_H
-2
View File
@@ -97,8 +97,6 @@ public:
MOCK_METHOD0(GetActiveView, class CViewport* ());
MOCK_METHOD1(SetActiveView, void(CViewport*));
MOCK_METHOD0(GetFileMonitor, struct IEditorFileMonitor* ());
MOCK_METHOD1(RegisterEventLoopHook, void(IEventLoopHook* ));
MOCK_METHOD1(UnregisterEventLoopHook, void(IEventLoopHook* ));
MOCK_CONST_METHOD0(CreateQMimeData, QMimeData* ());
MOCK_CONST_METHOD1(DestroyQMimeData, void(QMimeData*));
MOCK_METHOD0(GetLevelIndependentFileMan, class CLevelIndependentFileMan* ());
-20
View File
@@ -11,9 +11,6 @@
#include "Plugin.h"
// Editor
#include "Include/IViewPane.h"
#include <QMessageBox>
CClassFactory* CClassFactory::s_pInstance = nullptr;
@@ -149,23 +146,6 @@ IClassDesc* CClassFactory::FindClass(const GUID& rClassID) const
return pClassDesc;
}
IViewPaneClass* CClassFactory::FindViewPaneClassByTitle(const char* pPaneTitle) const
{
for (size_t i = 0; i < m_classes.size(); i++)
{
IViewPaneClass* viewPane = nullptr;
IClassDesc* desc = m_classes[i];
if (SUCCEEDED(desc->QueryInterface(__az_uuidof(IViewPaneClass), (void**)&viewPane)))
{
if (QString::compare(viewPane->GetPaneTitle(), pPaneTitle) == 0)
{
return viewPane;
}
}
}
return nullptr;
}
void CClassFactory::UnregisterClass(const char* pClassName)
{
IClassDesc* pClassDesc = FindClass(pClassName);
-8
View File
@@ -32,8 +32,6 @@ public:
IClassDesc* FindClass(const char* className) const;
//! Find class in the factory by class ID
IClassDesc* FindClass(const GUID& rClassID) const;
//! Find View Pane Class in the factory by pane title
IViewPaneClass* FindViewPaneClassByTitle(const char* pPaneTitle) const;
void UnregisterClass(const char* pClassName);
void UnregisterClass(const GUID& rClassID);
//! Get classes matching specific requirements ordered alphabetically by name.
@@ -96,10 +94,4 @@ public:
#define REGISTER_CLASS_DESC(ClassDesc) \
CAutoRegisterClassHelper g_AutoRegHelper##ClassDesc(new ClassDesc);
#define REGISTER_QT_CLASS_DESC(ClassDesc, name, category) \
CAutoRegisterClassHelper g_AutoRegHelper##ClassDesc(new CQtViewClass<ClassDesc>(name, category));
#define REGISTER_QT_CLASS_DESC_SYSTEM_ID(ClassDesc, name, category, systemid) \
CAutoRegisterClassHelper g_AutoRegHelper##ClassDesc(new CQtViewClass<ClassDesc>(name, category, systemid));
#endif // CRYINCLUDE_EDITOR_PLUGIN_H
@@ -209,12 +209,10 @@ namespace ProjectSettingsTool
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::PackageName))
->Attribute(Attributes::LinkOptional, true)
->Attribute(Attributes::PropertyIdentfier, Identfiers::AndroidPackageName)
->Attribute(Attributes::LinkedProperty, Identfiers::IosBundleIdentifer)
->DataElement(Handlers::LinkedLineEdit, &AndroidSettings::m_versionName, "Version Name", "Human readable version number. Used to set the \"android: versionName\" tag in the AndroidManifest.xml and ultimately what will be displayed in the App Store.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSVersionNumber))
->Attribute(Attributes::LinkOptional, true)
->Attribute(Attributes::PropertyIdentfier, Identfiers::AndroidVersionName)
->Attribute(Attributes::LinkedProperty, Identfiers::IosVersionName)
->DataElement(AZ::Edit::UIHandlers::Default, &AndroidSettings::m_versionNumber, "Version Number", "Internal application version number. Used to set the \"android:versionCode\" tag in the AndroidManifest.xml.")
->Attribute(AZ::Edit::Attributes::Min, 1)
->Attribute(AZ::Edit::Attributes::Max, Validators::maxAndroidVersion)
@@ -37,19 +37,15 @@ namespace ProjectSettingsTool
->DataElement(Handlers::LinkedLineEdit, &BaseSettings::m_projectName, "Project Name", "The name of the project.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName))
->Attribute(Attributes::PropertyIdentfier, Identfiers::ProjectName)
->Attribute(Attributes::LinkedProperty, Identfiers::IosBundleName)
->DataElement(Handlers::LinkedLineEdit, &BaseSettings::m_productName, "Product Name", "The project's user facing name.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IsNotEmpty))
->Attribute(Attributes::PropertyIdentfier, Identfiers::ProductName)
->Attribute(Attributes::LinkedProperty, Identfiers::IosDisplayName)
->DataElement(Handlers::LinkedLineEdit, &BaseSettings::m_executableName, "Executable Name", "The project launcher's name.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName))
->Attribute(Attributes::PropertyIdentfier, Identfiers::ExecutableName)
->Attribute(Attributes::LinkedProperty, Identfiers::IosExecutableName)
->DataElement(Handlers::QValidatedLineEdit, &BaseSettings::m_projectPath, "Project Path", "The project root folder path .")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileNameOrEmpty))
->Attribute(Attributes::PropertyIdentfier, Identfiers::ProductName)
->Attribute(Attributes::LinkedProperty, Identfiers::ExecutableName)
->DataElement(Handlers::QValidatedLineEdit, &BaseSettings::m_projectOutputFolder, "Output Folder", "The folder the packed project will be exported to.")
->DataElement(Handlers::QValidatedLineEdit, &BaseSettings::m_codeFolder, "Code Folder (legacy)", "A legacy setting specifing the folder for this project's code.")
;
@@ -262,27 +262,22 @@ namespace ProjectSettingsTool
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_bundleName, "Bundle Name", "The name of the bundle.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName))
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSFileName))
->Attribute(Attributes::PropertyIdentfier, Identfiers::IosBundleName)
->Attribute(Attributes::LinkedProperty, Identfiers::ProjectName)
->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_bundleDisplayName, "Display Name", "The user visible name of the bundle.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IsNotEmpty))
->Attribute(Attributes::PropertyIdentfier, Identfiers::IosDisplayName)
->Attribute(Attributes::LinkedProperty, Identfiers::ProductName)
->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_executableName, "Executable Name", "Name of the bundle's executable file.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::FileName))
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSFileName))
->Attribute(Attributes::PropertyIdentfier, Identfiers::IosExecutableName)
->Attribute(Attributes::LinkedProperty, Identfiers::ExecutableName)
->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_bundleIdentifier, "Bundle Identifier", "Uniquely identifies the bundle. Should be in reverse-DNS format.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::PackageName))
->Attribute(Attributes::LinkOptional, true)
->Attribute(Attributes::PropertyIdentfier, Identfiers::IosBundleIdentifer)
->Attribute(Attributes::LinkedProperty, Identfiers::AndroidPackageName)
->DataElement(Handlers::LinkedLineEdit, &IosSettings::m_versionName, "Version Name", "The release version number string for the app. Displayed in the app store.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSVersionNumber))
->Attribute(Attributes::LinkOptional, true)
->Attribute(Attributes::PropertyIdentfier, Identfiers::IosVersionName)
->Attribute(Attributes::LinkedProperty, Identfiers::AndroidVersionName)
->DataElement(Handlers::QValidatedLineEdit, &IosSettings::m_versionNumber, "Version Number", "The build version number string for the bundle.")
->Attribute(Attributes::FuncValidator, ConvertFunctorToVoid(&Validators::IOSVersionNumber))
->DataElement(AZ::Edit::UIHandlers::ComboBox, &IosSettings::m_developmentRegion, "Development Region", "The default language and region for the app.")
@@ -22,7 +22,6 @@ namespace ProjectSettingsTool
static const AZ::Crc32 Obfuscated = AZ_CRC("ObfuscatedText");
// Used as a tooltip and for distinguising linked properties
static const AZ::Crc32 PropertyIdentfier = AZ_CRC("PropertyIdentfier");
static const AZ::Crc32 LinkedProperty = AZ_CRC("LinkedProperty");
static const AZ::Crc32 DefaultPath = AZ_CRC("DefaultPath");
static const AZ::Crc32 DefaultImagePreview = AZ_CRC("DefaultImagePreview");
static const AZ::Crc32 ObfuscatedText = AZ_CRC("ObfuscatedText");
@@ -225,25 +225,6 @@ namespace ProjectSettingsTool
}
}
}
else if (attrib == Attributes::LinkedProperty)
{
AZStd::string linked;
if (attrValue->Read<AZStd::string>(linked))
{
auto result = m_ctrlToIdentAndLink.find(GUI);
if (result != m_ctrlToIdentAndLink.end())
{
result->second.linkedIdentifier = linked;
}
else
{
m_ctrlToIdentAndLink.insert(AZStd::pair<PropertyLinkedCtrl*, IdentAndLink>(GUI, IdentAndLink{ "", linked }));
m_ctrlInitOrder.push_back(GUI);
}
GUI->SetLinkTooltip(linked.data());
}
}
else
{
GUI->ConsumeAttribute(attrib, attrValue, debugName);
@@ -106,6 +106,11 @@ namespace ProjectSettingsTool
return RegularExpressionValidator("[\\w,-]+", name);
}
// Returns true if valid iOS file or directory name
RetType IOSFileName(const QString& name)
{
return RegularExpressionValidator("[\\w,-.]+", name);
}
RetType FileNameOrEmpty(const QString& name)
{
if (IsNotEmpty(name).first == QValidator::Acceptable)
@@ -24,6 +24,8 @@ namespace ProjectSettingsTool
// Returns true if valid cross platform file or directory name
FunctorValidator::ReturnType FileName(const QString& name);
// Returns true if valid iOS file or directory name
FunctorValidator::ReturnType IOSFileName(const QString& name);
// Returns true if valid cross platform file or directory name or empty
FunctorValidator::ReturnType FileNameOrEmpty(const QString& name);
// Returns true if string isn't empty
-40
View File
@@ -13,7 +13,6 @@
#include "IEditor.h"
#include "Include/IEditorClassFactory.h"
#include "Include/IViewPane.h"
#include "Include/ObjectEvent.h"
#include "Objects/ClassDesc.h"
@@ -107,43 +106,4 @@ public:
}
};
template<class TWidget>
class CQtViewClass
: public IViewPaneClass
{
public:
const char* m_name;
const char* m_category;
ESystemClassID m_classId;
CQtViewClass(const char* name, const char* category, ESystemClassID classId = ESYSTEM_CLASS_VIEWPANE)
: m_name(name)
, m_category(category)
, m_classId(classId)
{
}
ESystemClassID SystemClassID() override { return m_classId; };
static const GUID& GetClassID()
{
return TWidget::GetClassID();
}
const GUID& ClassID() override
{
return GetClassID();
}
QString ClassName() override { return m_name; };
QString Category() override { return m_category; };
QObject* CreateQObject() const override { return new TWidget(); };
QString GetPaneTitle() override { return m_name; };
unsigned int GetPaneTitleID() const override { return 0; };
EDockingDirection GetDockingDirection() override { return DOCK_FLOAT; };
QRect GetPaneRect() override { return {}; /* KDAB_TODO: ;m_sizeOptions.m_paneRect; */};
bool SinglePane() override { return false; };
bool WantIdleUpdate() override { return true; };
QSize GetMinSize() override { return {}; /*return m_sizeOptions.m_minSize;*/ }
};
#endif // CRYINCLUDE_EDITORCOMMON_QTVIEWPANE_H
-8
View File
@@ -269,14 +269,6 @@ bool RegisterQtViewPaneWithName([[maybe_unused]] IEditor* editor, const QString&
return true;
}
template<class TWidget>
void UnregisterQtViewPane()
{
// always close any views that the pane is responsible for before you remove it!
GetIEditor()->CloseView(CQtViewClass<TWidget>::GetClassID());
GetIEditor()->GetClassFactory()->UnregisterClass(CQtViewClass<TWidget>::GetClassID());
}
template<typename TWidget>
TWidget* FindViewPane(const QString& name)
{
@@ -143,5 +143,3 @@ void C2DBezierKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& se
}
}
}
REGISTER_QT_CLASS_DESC(C2DBezierKeyUIControls, "TrackView.KeyUI.2DBezier", "TrackViewKeyUI");
@@ -220,5 +220,3 @@ void CAssetBlendKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle&
}
}
}
REGISTER_QT_CLASS_DESC(CAssetBlendKeyUIControls, "TrackView.KeyUI.AssetBlends", "TrackViewKeyUI");
@@ -143,5 +143,3 @@ void CCaptureKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& sel
}
}
}
REGISTER_QT_CLASS_DESC(CCaptureKeyUIControls, "TrackView.KeyUI.Capture", "TrackViewKeyUI");
@@ -169,5 +169,3 @@ void CCommentKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& sel
}
}
}
REGISTER_QT_CLASS_DESC(CCommentKeyUIControls, "TrackView.KeyUI.Comment", "TrackViewKeyUI");
@@ -118,5 +118,3 @@ void CConsoleKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& sel
}
}
}
REGISTER_QT_CLASS_DESC(CConsoleKeyUIControls, "TrackView.KeyUI.Console", "TrackViewKeyUI");
@@ -149,5 +149,3 @@ void CEventKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& selec
}
}
}
REGISTER_QT_CLASS_DESC(CEventKeyUIControls, "TrackView.KeyUI.Event", "TrackViewKeyUI");
@@ -121,5 +121,3 @@ void CGotoKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& select
}
}
}
REGISTER_QT_CLASS_DESC(CGotoKeyUIControls, "TrackView.KeyUI.Goto", "TrackViewKeyUI");
@@ -167,5 +167,3 @@ void CScreenFaderKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle&
}
}
}
REGISTER_QT_CLASS_DESC(CScreenFaderKeyUIControls, "TrackView.KeyUI.ScreenFader", "TrackViewKeyUI");
@@ -269,5 +269,3 @@ void CSelectKeyUIControls::ResetCameraEntries()
OnCameraAdded(cameraComponentEntities.values[i]);
}
}
REGISTER_QT_CLASS_DESC(CSelectKeyUIControls, "TrackView.KeyUI.Select", "TrackViewKeyUI");
@@ -215,5 +215,3 @@ void CSequenceKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& se
}
}
}
REGISTER_QT_CLASS_DESC(CSequenceKeyUIControls, "TrackView.KeyUI.Sequence", "TrackViewKeyUI");
@@ -127,5 +127,3 @@ void CSoundKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& selec
}
}
}
REGISTER_QT_CLASS_DESC(CSoundKeyUIControls, "TrackView.KeyUI.Sound", "TrackViewKeyUI");
@@ -122,5 +122,3 @@ void CTimeRangeKeyUIControls::OnUIChange(IVariable* pVar, CTrackViewKeyBundle& s
}
}
}
REGISTER_QT_CLASS_DESC(CTimeRangeKeyUIControls, "TrackView.KeyUI.TimeRange", "TrackViewKeyUI");
@@ -235,5 +235,3 @@ void CTrackEventKeyUIControls::BuildEventDropDown(QString& curEvent, const QStri
}
}
}
REGISTER_QT_CLASS_DESC(CTrackEventKeyUIControls, "TrackView.KeyUI.TrackEvent", "TrackViewKeyUI");
-1
View File
@@ -17,7 +17,6 @@
#include "Cry_Geo.h"
#include "Viewport.h"
#include "Include/IViewPane.h"
#include "QtViewPaneManager.h"
// forward declaration.
class CLayoutWnd;
-2
View File
@@ -270,7 +270,6 @@ set(FILES
Include/ICommandManager.h
Include/IDisplayViewport.h
Include/IEditorClassFactory.h
Include/IEventLoopHook.h
Include/IExportManager.h
Include/IGizmoManager.h
Include/IIconManager.h
@@ -281,7 +280,6 @@ set(FILES
Include/IPreferencesPage.h
Include/ISourceControl.h
Include/ITransformManipulator.h
Include/IViewPane.h
Include/ObjectEvent.h
Util/AffineParts.cpp
Objects/BaseObject.cpp
@@ -92,6 +92,12 @@ namespace AZ::Data
result.Combine(resultHint);
}
if (SerializedAssetTracker* assetTracker = context.GetMetadata().Find<SerializedAssetTracker>();
assetTracker != nullptr && result.GetProcessing() == JSR::Processing::Completed)
{
assetTracker->AddAsset(*instance);
}
return context.Report(result,
result.GetProcessing() == JSR::Processing::Completed ? "Successfully stored Asset<T>." : "Failed to store Asset<T>.");
}
@@ -67,8 +67,7 @@ namespace AZ::Debug
Profiler() = default;
virtual ~Profiler() = default;
// support for the extra macro args (e.g. format strings) will come in a later PR
virtual void BeginRegion(const Budget* budget, const char* eventName) = 0;
virtual void BeginRegion(const Budget* budget, const char* eventName, size_t eventNameArgCount, ...) = 0;
virtual void EndRegion(const Budget* budget) = 0;
};
@@ -76,12 +75,11 @@ namespace AZ::Debug
{
public:
template<typename... T>
static void BeginRegion([[maybe_unused]] Budget* budget, [[maybe_unused]] const char* eventName, [[maybe_unused]] T const&... args);
static void EndRegion([[maybe_unused]] Budget* budget);
static void BeginRegion(Budget* budget, const char* eventName, T const&... args);
static void EndRegion(Budget* budget);
template<typename... T>
ProfileScope(Budget* budget, char const* eventName, T const&... args);
ProfileScope(Budget* budget, const char* eventName, T const&... args);
~ProfileScope();
@@ -31,10 +31,10 @@ namespace AZ::Debug
if (auto profiler = AZ::Interface<Profiler>::Get(); profiler)
{
profiler->BeginRegion(budget, eventName);
profiler->BeginRegion(budget, eventName, sizeof...(T), args...);
}
}
#endif // #if !defined(_RELEASE)
#endif // !defined(_RELEASE)
}
inline void ProfileScope::EndRegion([[maybe_unused]] Budget* budget)
@@ -55,7 +55,7 @@ namespace AZ::Debug
}
template<typename... T>
ProfileScope::ProfileScope(Budget* budget, char const* eventName, T const&... args)
ProfileScope::ProfileScope(Budget* budget, const char* eventName, T const&... args)
: m_budget{ budget }
{
BeginRegion(budget, eventName, args...);
@@ -203,7 +203,7 @@ namespace AZ
JsonSerializationResult::Result JsonMapSerializer::LoadElement(void* outputValue, SerializeContext::IDataContainer* container,
const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer,
const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement,
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context)
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, bool isMultiMap)
{
namespace JSR = JsonSerializationResult;
@@ -231,8 +231,30 @@ namespace AZ
return context.Report(keyResult, "Failed to read key for associative container.");
}
void* valueAddress = nullptr;
bool keyExists = false;
// For multimaps, we append values to keys instead updating them.
// This is to ensure legacy multimap serialization support.
if (!isMultiMap)
{
auto associativeContainer = container->GetAssociativeContainerInterface();
void* existingKeyValuePair = associativeContainer->GetElementByKey(outputValue, keyElement, keyAddress);
if (existingKeyValuePair)
{
valueAddress = pairContainer->GetElementByIndex(existingKeyValuePair, pairElement, 1);
expectedSize--;
keyExists = true;
}
}
// If the key doesn't exist or it's a multimap, we're adding the new element we reserved above.
if (!keyExists)
{
valueAddress = pairContainer->GetElementByIndex(address, pairElement, 1);
}
// Load value
void* valueAddress = pairContainer->GetElementByIndex(address, pairElement, 1);
AZ_Assert(valueAddress, "Element reserved for associative container, but unable to retrieve address of the value.");
ContinuationFlags valueLoadFlags = ContinuationFlags::LoadAsNewInstance;
if (valueElement->m_flags & SerializeContext::ClassElement::Flags::FLG_POINTER)
@@ -257,7 +279,18 @@ namespace AZ
}
else
{
container->StoreElement(outputValue, address);
// Even if the key exists, calling StoreElement will not replace the existing key
// and will free the temporary address as expected. Checking if the key already
// exists and skipping the call to StoreElement if it does, makes the intent more
// clear. The end result is the same either way.
if (!keyExists)
{
container->StoreElement(outputValue, address);
}
else
{
container->FreeReservedElement(outputValue, address, context.GetSerializeContext());
}
if (container->Size(outputValue) != expectedSize)
{
return context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Unavailable,
@@ -430,7 +463,7 @@ namespace AZ
JsonSerializationResult::Result JsonUnorderedMultiMapSerializer::LoadElement(void* outputValue, SerializeContext::IDataContainer* container,
const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer,
const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement,
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context)
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, [[maybe_unused]] bool isMultiMap)
{
namespace JSR = JsonSerializationResult;
@@ -440,7 +473,7 @@ namespace AZ
for (auto& entry : value.GetArray())
{
result.Combine(JsonMapSerializer::LoadElement(outputValue, container, pairElement, pairContainer,
keyElement, valueElement, key, entry, context));
keyElement, valueElement, key, entry, context, true));
if (result.GetProcessing() == JSR::Processing::Halted)
{
return context.Report(result, "Unable to process the key or all values in multi-map.");
@@ -451,7 +484,7 @@ namespace AZ
else if (IsExplicitDefault(value))
{
return JsonMapSerializer::LoadElement(outputValue, container, pairElement, pairContainer,
keyElement, valueElement, key, value, context);
keyElement, valueElement, key, value, context, true);
}
else
{
@@ -32,7 +32,7 @@ namespace AZ
virtual JsonSerializationResult::Result LoadElement(void* outputValue, SerializeContext::IDataContainer* container,
const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer,
const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement,
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context);
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, bool isMultiMap = false);
virtual JsonSerializationResult::Result Store(rapidjson::Value& outputValue, const void* inputValue, const void* defaultValue,
const Uuid& valueTypeId, JsonSerializerContext& context, bool sortResult);
@@ -62,7 +62,7 @@ namespace AZ
JsonSerializationResult::Result LoadElement(void* outputValue, SerializeContext::IDataContainer* container,
const SerializeContext::ClassElement* pairElement, SerializeContext::IDataContainer* pairContainer,
const SerializeContext::ClassElement* keyElement, const SerializeContext::ClassElement* valueElement,
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context) override;
const rapidjson::Value& key, const rapidjson::Value& value, JsonDeserializerContext& context, bool isMultiMap = false) override;
using JsonMapSerializer::Store;
JsonSerializationResult::Result Store(rapidjson::Value& outputValue, const void* inputValue, const void* defaultValue,
@@ -475,7 +475,7 @@ namespace JsonSerializationTests
EXPECT_STRCASEEQ("value_42", worldKey->second.m_value.c_str());
}
TEST_F(JsonMapSerializerTests, Load_DuplicateKey_EntryIgnored)
TEST_F(JsonMapSerializerTests, Load_DuplicateKey_EntryUpdated)
{
using namespace AZ::JsonSerializationResult;
@@ -489,12 +489,12 @@ namespace JsonSerializationTests
StringMap values;
ResultCode result = m_unorderedMapSerializer.Load(&values, azrtti_typeid(&values), *m_jsonDocument, *m_jsonDeserializationContext);
EXPECT_EQ(Processing::PartialAlter, result.GetProcessing());
EXPECT_EQ(Outcomes::Unavailable, result.GetOutcome());
EXPECT_EQ(Processing::Completed, result.GetProcessing());
EXPECT_EQ(Outcomes::Success, result.GetOutcome());
auto entry = values.find("Hello");
ASSERT_NE(values.end(), entry);
EXPECT_STRCASEEQ("World", entry->second.c_str());
EXPECT_EQ("Other", entry->second);
}
TEST_F(JsonMapSerializerTests, Load_DuplicateMultiKey_LoadEverything)
@@ -536,8 +536,8 @@ namespace JsonSerializationTests
ResultCode result = m_unorderedMapSerializer.Load(&values,
azrtti_typeid(&values), *m_jsonDocument, *m_jsonDeserializationContext);
EXPECT_EQ(Processing::Altered, result.GetProcessing());
EXPECT_EQ(Outcomes::Unavailable, result.GetOutcome());
EXPECT_EQ(Processing::Completed, result.GetProcessing());
EXPECT_EQ(Outcomes::Success, result.GetOutcome());
auto entry = values.find("Hello");
ASSERT_NE(values.end(), entry);
@@ -28,7 +28,7 @@ namespace AzToolsFramework
{
namespace Internal
{
AZ::JsonSerializationResult::ResultCode JsonIssueReporter(AZStd::string& scratchBuffer,
static AZ::JsonSerializationResult::ResultCode JsonIssueReporter(AZStd::string& scratchBuffer,
AZStd::string_view message, AZ::JsonSerializationResult::ResultCode result, AZStd::string_view path)
{
namespace JSR = AZ::JsonSerializationResult;
@@ -48,6 +48,66 @@ namespace AzToolsFramework
return result;
}
static bool StoreInstanceInPrefabDom(
const Instance& instance,
PrefabDom& prefabDom,
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>* referencedAssets,
StoreFlags flags)
{
InstanceEntityIdMapper entityIdMapper;
entityIdMapper.SetStoringInstance(instance);
// Need to store the id mapper as both its type and its base type
// Metadata is found by type id and we need access to both types at different levels (Instance, EntityId)
AZ::JsonSerializerSettings settings;
settings.m_metadata.Add(static_cast<AZ::JsonEntityIdSerializer::JsonEntityIdMapper*>(&entityIdMapper));
settings.m_metadata.Add(&entityIdMapper);
if (referencedAssets)
{
settings.m_metadata.Add(AZ::Data::SerializedAssetTracker{});
}
if ((flags & StoreFlags::StripDefaultValues) != StoreFlags::StripDefaultValues)
{
settings.m_keepDefaults = true;
}
if ((flags & StoreFlags::StoreLinkIds) != StoreFlags::None)
{
settings.m_metadata.Create<LinkIdMetadata>();
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](
AZStd::string_view message, AZ::JsonSerializationResult::ResultCode result,
AZStd::string_view path) -> AZ::JsonSerializationResult::ResultCode
{
return Internal::JsonIssueReporter(scratchBuffer, message, result, path);
};
settings.m_reporting = AZStd::move(issueReportingCallback);
AZ::JsonSerializationResult::ResultCode result =
AZ::JsonSerialization::Store(prefabDom, prefabDom.GetAllocator(), instance, settings);
if (result.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted)
{
AZ_Error(
"Prefab", false,
"Failed to serialize prefab instance with source path %s. "
"Unable to proceed.",
instance.GetTemplateSourcePath().c_str());
return false;
}
if (referencedAssets)
{
*referencedAssets = AZStd::move(settings.m_metadata.Find<AZ::Data::SerializedAssetTracker>()->GetTrackedAssets());
}
return true;
}
}
PrefabDomValueReference FindPrefabDomValue(PrefabDomValue& parentValue, const char* valueName)
@@ -74,49 +134,16 @@ namespace AzToolsFramework
bool StoreInstanceInPrefabDom(const Instance& instance, PrefabDom& prefabDom, StoreFlags flags)
{
InstanceEntityIdMapper entityIdMapper;
entityIdMapper.SetStoringInstance(instance);
return Internal::StoreInstanceInPrefabDom(instance, prefabDom, nullptr, flags);
}
// Need to store the id mapper as both its type and its base type
// Meta data is found by type id and we need access to both types at different levels (Instance, EntityId)
AZ::JsonSerializerSettings settings;
settings.m_metadata.Add(static_cast<AZ::JsonEntityIdSerializer::JsonEntityIdMapper*>(&entityIdMapper));
settings.m_metadata.Add(&entityIdMapper);
if ((flags & StoreFlags::StripDefaultValues) != StoreFlags::StripDefaultValues)
{
settings.m_keepDefaults = true;
}
if ((flags & StoreFlags::StoreLinkIds) != StoreFlags::None)
{
settings.m_metadata.Create<LinkIdMetadata>();
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer]
(AZStd::string_view message, AZ::JsonSerializationResult::ResultCode result,
AZStd::string_view path) -> AZ::JsonSerializationResult::ResultCode
{
return Internal::JsonIssueReporter(scratchBuffer, message, result, path);
};
settings.m_reporting = AZStd::move(issueReportingCallback);
AZ::JsonSerializationResult::ResultCode result =
AZ::JsonSerialization::Store(prefabDom, prefabDom.GetAllocator(), instance, settings);
if (result.GetProcessing() == AZ::JsonSerializationResult::Processing::Halted)
{
AZ_Error("Prefab", false,
"Failed to serialize prefab instance with source path %s. "
"Unable to proceed.",
instance.GetTemplateSourcePath().c_str());
return false;
}
return true;
bool StoreInstanceInPrefabDom(
const Instance& instance,
PrefabDom& prefabDom,
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets,
StoreFlags flags)
{
return Internal::StoreInstanceInPrefabDom(instance, prefabDom, &referencedAssets, flags);
}
bool StoreEntityInPrefabDomFormat(const AZ::Entity& entity, Instance& owningInstance, PrefabDom& prefabDom, StoreFlags flags)
@@ -57,14 +57,28 @@ namespace AzToolsFramework
AZ_DEFINE_ENUM_BITWISE_OPERATORS(StoreFlags);
/**
* Stores a valid Prefab Instance within a Prefab Dom. Useful for generating Templates
* @param instance The instance to store
* @param prefabDom The prefabDom that will be used to store the Instance data
* @param flags Controls behavior such as whether to store default values
* @return bool on whether the operation succeeded
* Stores a valid Prefab Instance within a Prefab Dom. Useful for generating Templates.
* @param instance The instance to store.
* @param prefabDom The prefabDom that will be used to store the Instance data.
* @param flags Controls behavior such as whether to store default values.
* @return bool on whether the operation succeeded.
*/
bool StoreInstanceInPrefabDom(const Instance& instance, PrefabDom& prefabDom, StoreFlags flags = StoreFlags::None);
/**
* Stores a valid Prefab Instance within a Prefab Dom. Useful for generating Templates.
* @param instance The instance to store.
* @param prefabDom The prefabDom that will be used to store the Instance data.
* @param referencedAssets Collect a list of the assets that are referenced during storing.
* @param flags Controls behavior such as whether to store default values.
* @return bool on whether the operation succeeded.
*/
bool StoreInstanceInPrefabDom(
const Instance& instance,
PrefabDom& prefabDom,
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets,
StoreFlags flags = StoreFlags::None);
/**
* Stores a valid entity in Prefab Dom format.
* @param entity The entity to store
@@ -6,12 +6,14 @@
*
*/
#include <AzToolsFramework/Prefab/Spawnable/InMemorySpawnableAssetContainer.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Asset/AssetManagerBus.h>
#include <AzCore/Asset/AssetSerializer.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzToolsFramework/Prefab/PrefabLoader.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
#include <AzToolsFramework/Prefab/Spawnable/InMemorySpawnableAssetContainer.h>
#include <AzToolsFramework/Prefab/Spawnable/PrefabConverterStackProfileNames.h>
namespace AzToolsFramework::Prefab::PrefabConversionUtils
@@ -161,13 +163,10 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
return AZ::Failure(AZStd::string::format("Failed to produce the target spawnable '%.*s'.", AZ_STRING_ARG(spawnableName)));
}
if (loadReferencedAssets)
{
for (auto& product : context.GetProcessedObjects())
{
LoadReferencedAssets(product.GetReferencedAssets());
}
LoadReferencedAssets(spawnableAssetData);
}
auto& spawnableAssetDataAdded = m_spawnableAssets.emplace(spawnableName, spawnableAssetData).first->second;
@@ -213,63 +212,87 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
return m_spawnableAssets;
}
void InMemorySpawnableAssetContainer::LoadReferencedAssets(AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets)
void InMemorySpawnableAssetContainer::LoadReferencedAssets(SpawnableAssetData& spawnable)
{
// Start our loads on all assets by calling GetAsset from the AssetManager
for (AZ::Data::Asset<AZ::Data::AssetData>& asset : referencedAssets)
// Get the referenced assets directly from the product. This is done for two reasons:
// 1. Avoids calls to the Asset Manager for assets that are already loaded.
// 2. Gets the exact asset to load to avoid issues with assets that don't reload.
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>*> blockingAssets;
AZ::SerializeContext* sc = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(sc, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
AZ_Assert(sc, "Unable to locate Serialize Context while resolving asset references in the in-memory spawnable asset container.");
for (AZ::Data::Asset<AZ::Data::AssetData>& asset : spawnable.m_assets)
{
if (!asset.GetId().IsValid())
auto callback = [&blockingAssets](
void* object, const AZ::SerializeContext::ClassData* classData,
[[maybe_unused]]const AZ::SerializeContext::ClassElement* elementData) -> bool
{
AZ_Error("Prefab", false, "Invalid asset found referenced in scene while entering game mode");
continue;
}
if (classData->m_typeId == AZ::GetAssetClassId())
{
auto asset = reinterpret_cast<AZ::Data::Asset<AZ::Data::AssetData>*>(object);
const AZ::Data::AssetLoadBehavior loadBehavior = asset.GetAutoLoadBehavior();
if (!asset->GetId().IsValid())
{
AZ_Error(
"Prefab", false,
"Invalid asset found referenced in scene while entering game mode. The asset was stored in an instance of %s.",
classData->m_name);
return false;
}
if (loadBehavior == AZ::Data::AssetLoadBehavior::NoLoad)
{
continue;
}
if (asset->GetStatus() != AZ::Data::AssetData::AssetStatus::NotLoaded)
{
// Already loaded so no need to do anything.
return false;
}
AZ::Data::AssetId assetId = asset.GetId();
AZ::Data::AssetType assetType = asset.GetType();
const AZ::Data::AssetLoadBehavior loadBehavior = asset->GetAutoLoadBehavior();
if (loadBehavior == AZ::Data::AssetLoadBehavior::NoLoad)
{
return false;
}
asset = AZ::Data::AssetManager::Instance().GetAsset(assetId, assetType, loadBehavior);
if (loadBehavior == AZ::Data::AssetLoadBehavior::PreLoad)
{
// Only assets that are preloaded need to be waited on.
blockingAssets.push_back(asset);
}
if (!asset->QueueLoad())
{
AZ_Error(
"Prefab", false, "Failed to queue asset '%s' (%s) of type '%s' for loading while entering game mode.",
asset->GetHint().c_str(), asset->GetId().ToString<AZStd::fixed_string<AZ::Uuid::MaxStringBuffer>>().c_str(),
asset->GetType().ToString<AZStd::fixed_string<AZ::Uuid::MaxStringBuffer>>().c_str());
return false;
}
if (!asset.GetId().IsValid())
{
AZ_Error("Prefab", false, "Invalid asset found referenced in scene while entering game mode");
continue;
}
return false;
}
return true;
};
AZ::SerializeContext::EnumerateInstanceCallContext enumerationContext(
callback, nullptr, sc, AZ::SerializeContext::ENUM_ACCESS_FOR_READ, nullptr);
sc->EnumerateInstance(&enumerationContext, asset.GetData(), asset.GetType(), nullptr, nullptr);
}
// For all Preload assets we block until they're ready
// We do this as a separate pass so that we don't interrupt queuing up all other asset loads
for (AZ::Data::Asset<AZ::Data::AssetData>& asset : referencedAssets)
for (AZ::Data::Asset<AZ::Data::AssetData>* asset : blockingAssets)
{
if (!asset.GetId().IsValid())
asset->BlockUntilLoadComplete();
if (asset->IsError())
{
AZ_Error("Prefab", false, "Invalid asset found referenced in scene while entering game mode");
continue;
}
const AZ::Data::AssetLoadBehavior loadBehavior = asset.GetAutoLoadBehavior();
if (loadBehavior != AZ::Data::AssetLoadBehavior::PreLoad)
{
continue;
}
asset.BlockUntilLoadComplete();
if (asset.IsError())
{
AZ_Error("Prefab", false, "Asset with id %s failed to preload while entering game mode",
asset.GetId().ToString<AZStd::string>().c_str());
AZ_Error(
"Prefab", false, "Asset '%s' (%s) of type '%s' failed to preload while entering game mode", asset->GetHint().c_str(),
asset->GetId().ToString<AZStd::fixed_string<AZ::Uuid::MaxStringBuffer>>().c_str(),
asset->GetType().ToString<AZStd::fixed_string<AZ::Uuid::MaxStringBuffer>>().c_str());
continue;
}
}
}
} // namespace AzToolsFramework::Prefab::PrefabConversionUtils
@@ -21,7 +21,6 @@ namespace AzToolsFramework::Prefab
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
class InMemorySpawnableAssetContainer
{
public:
@@ -58,8 +57,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
const SpawnableAssets& GetAllInMemorySpawnableAssets() const;
private:
void LoadReferencedAssets(AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets);
void LoadReferencedAssets(SpawnableAssetData& spawnable);
SpawnableAssets m_spawnableAssets;
PrefabConversionUtils::PrefabConversionPipeline m_converter;
AZStd::string_view m_stockProfile;
@@ -53,21 +53,14 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
const PrefabDom& PrefabDocument::GetDom() const
{
if (m_isDirty)
{
m_isDirty = !PrefabDomUtils::StoreInstanceInPrefabDom(*m_instance, m_dom);
}
RefreshPrefabDom();
return m_dom;
}
PrefabDom&& PrefabDocument::TakeDom()
{
if (m_isDirty)
{
[[maybe_unused]] bool storedSuccessfully = PrefabDomUtils::StoreInstanceInPrefabDom(*m_instance, m_dom);
AZ_Assert(storedSuccessfully, "Failed to store Instance '%s' to PrefabDom.", m_name.c_str());
m_isDirty = false;
}
RefreshPrefabDom();
// After the PrefabDom is moved an empty PrefabDom is left behind. This should be reflected in the Instance,
// so reset it so it's empty as well.
m_instance->Reset();
@@ -126,11 +119,13 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& PrefabDocument::GetReferencedAssets()
{
RefreshPrefabDom();
return m_referencedAssets;
}
const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& PrefabDocument::GetReferencedAssets() const
{
RefreshPrefabDom();
return m_referencedAssets;
}
@@ -158,4 +153,20 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
return false;
}
}
void PrefabDocument::RefreshPrefabDom() const
{
if (m_isDirty)
{
m_referencedAssets.clear();
if (PrefabDomUtils::StoreInstanceInPrefabDom(*m_instance, m_dom, m_referencedAssets))
{
m_isDirty = false;
}
else
{
AZ_Assert(false, "Failed to store Instance '%s' to PrefabDom.", m_name.c_str());
}
}
}
} // namespace AzToolsFramework::Prefab::PrefabConversionUtils
@@ -58,11 +58,13 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
private:
bool ConstructInstanceFromPrefabDom(const PrefabDom& prefab);
// Marked const so this function can be called from other const functions. It will only operate on mutable variables.
void RefreshPrefabDom() const;
mutable PrefabDom m_dom;
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> m_instance;
AZStd::string m_name;
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> m_referencedAssets;
mutable AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> m_referencedAssets;
mutable bool m_isDirty{ false };
};
} // namespace AzToolsFramework::Prefab::PrefabConversionUtils
@@ -80,7 +80,6 @@ namespace AzToolsFramework
EntityOutlinerListModel::EntityOutlinerListModel(QObject* parent)
: QAbstractItemModel(parent)
, m_entitySelectQueue()
, m_entityExpandQueue()
, m_entityChangeQueue()
, m_entityChangeQueued(false)
, m_entityLayoutQueued(false)
@@ -1275,7 +1274,6 @@ namespace AzToolsFramework
void EntityOutlinerListModel::QueueEntityToExpand(AZ::EntityId entityId, bool expand)
{
m_entityExpansionState[entityId] = expand;
m_entityExpandQueue.insert(entityId);
QueueEntityUpdate(entityId);
}
@@ -1300,16 +1298,7 @@ namespace AzToolsFramework
{
return;
}
{
AZ_PROFILE_SCOPE(Editor, "EntityOutlinerListModel::ProcessEntityUpdates:ExpandQueue");
for (auto entityId : m_entityExpandQueue)
{
emit ExpandEntity(entityId, IsExpanded(entityId));
};
m_entityExpandQueue.clear();
}
{
AZ_PROFILE_SCOPE(Editor, "EntityOutlinerListModel::ProcessEntityUpdates:SelectQueue");
for (auto entityId : m_entitySelectQueue)
@@ -156,7 +156,6 @@ namespace AzToolsFramework
void ProcessEntityUpdates();
Q_SIGNALS:
void ExpandEntity(const AZ::EntityId& entityId, bool expand);
void SelectEntity(const AZ::EntityId& entityId, bool select);
void EnableSelectionUpdates(bool enable);
void ResetFilter();
@@ -190,7 +189,6 @@ namespace AzToolsFramework
void QueueEntityToExpand(AZ::EntityId entityId, bool expand);
void ProcessEntityInfoResetEnd();
AZStd::unordered_set<AZ::EntityId> m_entitySelectQueue;
AZStd::unordered_set<AZ::EntityId> m_entityExpandQueue;
AZStd::unordered_set<AZ::EntityId> m_entityChangeQueue;
bool m_entityChangeQueued;
bool m_entityLayoutQueued;
@@ -81,6 +81,61 @@ namespace AzToolsFramework
update();
}
void EntityOutlinerTreeView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles)
{
AzQtComponents::StyledTreeView::dataChanged(topLeft, bottomRight, roles);
if (topLeft.isValid() && topLeft.parent() == bottomRight.parent() && topLeft.row() <= bottomRight.row() &&
topLeft.column() <= bottomRight.column())
{
for (int i = topLeft.row(); i <= bottomRight.row(); i++)
{
auto modelRow = topLeft.sibling(i, EntityOutlinerListModel::ColumnName);
if (modelRow.isValid())
{
checkExpandedState(modelRow);
}
}
}
}
void EntityOutlinerTreeView::rowsInserted(const QModelIndex& parent, int start, int end)
{
if (parent.isValid())
{
for (int i = start; i <= end; i++)
{
auto modelRow = model()->index(i, EntityOutlinerListModel::ColumnName, parent);
if (modelRow.isValid())
{
checkExpandedState(modelRow);
recursiveCheckExpandedStates(modelRow);
}
}
}
AzQtComponents::StyledTreeView::rowsInserted(parent, start, end);
}
void EntityOutlinerTreeView::recursiveCheckExpandedStates(const QModelIndex& current)
{
const int rowCount = model()->rowCount(current);
for (int i = 0; i < rowCount; i++)
{
auto modelRow = model()->index(i, EntityOutlinerListModel::ColumnName, current);
if (modelRow.isValid())
{
checkExpandedState(modelRow);
recursiveCheckExpandedStates(modelRow);
}
}
}
void EntityOutlinerTreeView::checkExpandedState(const QModelIndex& current)
{
const bool expandState = current.data(EntityOutlinerListModel::ExpandedRole).template value<bool>();
setExpanded(current, expandState);
}
void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event)
{
//postponing normal mouse pressed logic until mouse is released or dragged
@@ -51,6 +51,10 @@ namespace AzToolsFramework
Q_SIGNALS:
void ItemDropped();
protected Q_SLOTS:
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
void rowsInserted(const QModelIndex &parent, int start, int end) override;
protected:
// Qt overrides
void mousePressEvent(QMouseEvent* event) override;
@@ -75,6 +79,8 @@ namespace AzToolsFramework
void ClearQueuedMouseEvent();
void processQueuedMousePressedEvent(QMouseEvent* event);
void recursiveCheckExpandedStates(const QModelIndex& parent);
void checkExpandedState(const QModelIndex& current);
void StartCustomDrag(const QModelIndexList& indexList, Qt::DropActions supportedActions) override;
@@ -224,7 +224,6 @@ namespace AzToolsFramework
connect(m_gui->m_objectTree, &QTreeView::expanded, this, &EntityOutlinerWidget::OnTreeItemExpanded);
connect(m_gui->m_objectTree, &QTreeView::collapsed, this, &EntityOutlinerWidget::OnTreeItemCollapsed);
connect(m_gui->m_objectTree, &EntityOutlinerTreeView::ItemDropped, this, &EntityOutlinerWidget::OnDropEvent);
connect(m_listModel, &EntityOutlinerListModel::ExpandEntity, this, &EntityOutlinerWidget::OnExpandEntity);
connect(m_listModel, &EntityOutlinerListModel::SelectEntity, this, &EntityOutlinerWidget::OnSelectEntity);
connect(m_listModel, &EntityOutlinerListModel::EnableSelectionUpdates, this, &EntityOutlinerWidget::OnEnableSelectionUpdates);
connect(m_listModel, &EntityOutlinerListModel::ResetFilter, this, &EntityOutlinerWidget::ClearFilter);
@@ -972,10 +971,6 @@ namespace AzToolsFramework
m_listModel->OnEntityCollapsed(entityId);
}
void EntityOutlinerWidget::OnExpandEntity(const AZ::EntityId& entityId, bool expand)
{
m_gui->m_objectTree->setExpanded(GetIndexFromEntityId(entityId), expand);
}
void EntityOutlinerWidget::OnSelectEntity(const AZ::EntityId& entityId, bool selected)
{
@@ -155,7 +155,6 @@ namespace AzToolsFramework
void OnTreeItemDoubleClicked(const QModelIndex& index);
void OnTreeItemExpanded(const QModelIndex& index);
void OnTreeItemCollapsed(const QModelIndex& index);
void OnExpandEntity(const AZ::EntityId& entityId, bool expand);
void OnSelectEntity(const AZ::EntityId& entityId, bool selected);
void OnEnableSelectionUpdates(bool enable);
void OnDropEvent();
-14
View File
@@ -856,18 +856,4 @@ DLL_EXPORT void OutputDebugString(const char* outputString)
#endif
// This code does not have a long life span and will be replaced soon
#if defined(APPLE) || defined(LINUX) || defined(DEFINE_LEGACY_CRY_FILE_OPERATIONS)
bool CrySetFileAttributes(const char* lpFileName, uint32 dwFileAttributes)
{
//TODO: implement
printf("CrySetFileAttributes not properly implemented yet\n");
return false;
}
#endif //defined(APPLE) || defined(LINUX)
#endif // AZ_TRAIT_LEGACY_CRYCOMMON_USE_WINDOWS_STUBS
-1
View File
@@ -336,7 +336,6 @@ void SetFlags(T& dest, U flags, bool b)
#include AZ_RESTRICTED_FILE(platform_h)
#endif
bool CrySetFileAttributes(const char* lpFileName, uint32 dwFileAttributes);
threadID CryGetCurrentThreadId();
#ifdef __GNUC__
-17
View File
@@ -24,7 +24,6 @@
#define PLATFORM_IMPL_H_SECTION_TRAITS 1
#define PLATFORM_IMPL_H_SECTION_CRYLOWLATENCYSLEEP 2
#define PLATFORM_IMPL_H_SECTION_CRYGETFILEATTRIBUTES 3
#define PLATFORM_IMPL_H_SECTION_CRYSETFILEATTRIBUTES 4
#define PLATFORM_IMPL_H_SECTION_CRY_FILE_ATTRIBUTE_STUBS 5
#define PLATFORM_IMPL_H_SECTION_CRY_SYSTEM_FUNCTIONS 6
#define PLATFORM_IMPL_H_SECTION_VIRTUAL_ALLOCATORS 7
@@ -238,22 +237,6 @@ void InitRootDir(char szExeFileName[], uint nExeSize, char szExeRootName[], uint
}
}
//////////////////////////////////////////////////////////////////////////
bool CrySetFileAttributes(const char* lpFileName, uint32 dwFileAttributes)
{
#if defined(AZ_RESTRICTED_PLATFORM)
#define AZ_RESTRICTED_SECTION PLATFORM_IMPL_H_SECTION_CRYSETFILEATTRIBUTES
#include AZ_RESTRICTED_FILE(platform_impl_h)
#endif
#if defined(AZ_RESTRICTED_SECTION_IMPLEMENTED)
#undef AZ_RESTRICTED_SECTION_IMPLEMENTED
#else
AZStd::wstring lpFileNameW;
AZStd::to_wstring(lpFileNameW, lpFileName);
return SetFileAttributes(lpFileNameW.c_str(), dwFileAttributes) != 0;
#endif
}
//////////////////////////////////////////////////////////////////////////
threadID CryGetCurrentThreadId()
{
+4 -1
View File
@@ -1132,7 +1132,10 @@ bool CXmlNode::saveToFile(const char* fileName)
bool CXmlNode::saveToFile([[maybe_unused]] const char* fileName, size_t chunkSize, AZ::IO::HandleType fileHandle)
{
CrySetFileAttributes(fileName, FILE_ATTRIBUTE_NORMAL);
if (AZ::IO::SystemFile::Exists(fileName) && !AZ::IO::SystemFile::IsWritable(fileName))
{
AZ::IO::SystemFile::SetWritable(fileName, true);
}
if (chunkSize < 256 * 1024) // make at least 256k
{
@@ -30,6 +30,8 @@ set(FILES
native/tests/assetBuilderSDK/SerializationDependenciesTests.cpp
native/tests/assetmanager/AssetProcessorManagerTest.cpp
native/tests/assetmanager/AssetProcessorManagerTest.h
native/tests/assetmanager/ModtimeScanningTests.cpp
native/tests/assetmanager/ModtimeScanningTests.h
native/tests/utilities/assetUtilsTest.cpp
native/tests/platformconfiguration/platformconfigurationtests.cpp
native/tests/platformconfiguration/platformconfigurationtests.h
@@ -49,7 +49,7 @@ namespace UnitTests
}
struct PathDependencyBase
: UnitTest::TraceBusRedirector
: ::UnitTest::TraceBusRedirector
{
void Init();
void Destroy();
@@ -65,7 +65,7 @@ namespace UnitTests
};
struct PathDependencyDeletionTest
: UnitTest::ScopedAllocatorSetupFixture
: ::UnitTest::ScopedAllocatorSetupFixture
, PathDependencyBase
{
void SetUp() override
@@ -357,7 +357,7 @@ namespace UnitTests
}
struct PathDependencyBenchmarks
: UnitTest::ScopedAllocatorFixture
: ::UnitTest::ScopedAllocatorFixture
, PathDependencyBase
{
static inline constexpr int NumTestDependencies = 4; // Must be a multiple of 4
@@ -530,7 +530,7 @@ namespace UnitTests
BENCHMARK_F(PathDependencyBenchmarksWrapperClass, BM_DeferredWildcardDependencyResolution)(benchmark::State& state)
{
for (auto _ : state)
for ([[maybe_unused]] auto unused : state)
{
m_benchmarks->m_stateData->SetProductDependencies(m_benchmarks->m_dependencies);
@@ -191,7 +191,7 @@ namespace UnitTests
m_data->m_perforceComponent = AZStd::make_unique<MockPerforceComponent>();
m_data->m_perforceComponent->Activate();
m_data->m_perforceComponent->SetConnection(new UnitTest::MockPerforceConnection(m_command));
m_data->m_perforceComponent->SetConnection(new ::UnitTest::MockPerforceConnection(m_command));
}
void TearDown() override
@@ -876,7 +876,7 @@ namespace UnitTests
QDir tempPath(m_tempDir.path());
auto filePath = QDir(tempPath.absoluteFilePath(m_data->m_scanFolder1.m_scanFolder.c_str())).absoluteFilePath("duplicate/file1.tif");
ASSERT_TRUE(AZ::IO::FileIOBase::GetInstance()->Exists(filePath.toUtf8().constData()));
auto result = m_data->m_reporter->Delete(filePath.toUtf8().constData(), false);
@@ -22,108 +22,6 @@
using namespace AssetProcessor;
class AssetProcessorManager_Test
: public AssetProcessorManager
{
public:
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, AssetProcessedImpl_DifferentProductDependenciesPerProduct_SavesCorrectlyToDatabase);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_DeferredResolution);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, SameFilenameForAllPlatforms);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, DeleteFolder_SignalsDeleteOfContainedFiles);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDifferentTypes_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_Reverse_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_MissingFiles_ReturnsNoPathWithPlaceholders);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_BeforeComputingDirtiness_AllDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_EmptyDatabase_AllDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_SameAsLastTime_NoneDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_MoreThanLastTime_NewOneIsDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_FewerThanLastTime_Dirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPattern_CountsAsNew);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPatternType_CountsAsNew);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewPattern_CountsAsNewBuilder);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewVersionNumber_IsNotANewBuilder);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewAnalysisFingerprint_IsNotANewBuilder);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_UpdateTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid_UpdatesWhenTheyAppear);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName_UpdatesWhenTheyAppear);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardMissingFiles_ByName_UpdatesWhenTheyAppear);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, JobDependencyOrderOnce_MultipleJobs_EmitOK);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, SourceFileProcessFailure_ClearsFingerprint);
friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_DoesNotDuplicateDependency);
friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, AbsolutePathProductDependency_RetryDeferredDependenciesWithMatchingSource_DependencyResolves);
friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_ValidatePathDependenciesMap);
friend class GTEST_TEST_CLASS_NAME_(AbsolutePathProductDependencyTest, UnresolvedSourceFileTypeProductPathDependency_DependencyHasNoProductOutput_ValidatePathDependenciesMap);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_FileUnchanged);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile);
friend class GTEST_TEST_CLASS_NAME_(ModtimeScanningTest, ModtimeSkipping_DeleteFile);
friend class GTEST_TEST_CLASS_NAME_(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache);
friend class GTEST_TEST_CLASS_NAME_(MetadataFileTest, MetadataFile_SourceFileExtensionDifferentCase);
friend class AssetProcessorManagerTest;
friend struct ModtimeScanningTest;
friend struct JobDependencyTest;
friend struct ChainJobDependencyTest;
friend struct DeleteTest;
friend struct PathDependencyTest;
friend struct DuplicateProductsTest;
friend struct DuplicateProcessTest;
friend struct AbsolutePathProductDependencyTest;
friend struct WildcardSourceDependencyTest;
explicit AssetProcessorManager_Test(PlatformConfiguration* config, QObject* parent = nullptr);
~AssetProcessorManager_Test() override;
bool CheckJobKeyToJobRunKeyMap(AZStd::string jobKey);
int CountDirtyBuilders() const
{
int numDirty = 0;
for (const auto& element : m_builderDataCache)
{
if (element.second.m_isDirty)
{
++numDirty;
}
}
return numDirty;
}
bool IsBuilderDirty(const AZ::Uuid& builderBusId) const
{
auto finder = m_builderDataCache.find(builderBusId);
if (finder == m_builderDataCache.end())
{
return true;
}
return finder->second.m_isDirty;
}
};
AssetProcessorManager_Test::AssetProcessorManager_Test(AssetProcessor::PlatformConfiguration* config, QObject* parent /*= 0*/)
:AssetProcessorManager(config, parent)
{
@@ -3839,632 +3737,6 @@ TEST_F(AssetProcessorManagerTest, SourceFileProcessFailure_ClearsFingerprint)
ASSERT_EQ(source.m_analysisFingerprint, "");
}
void ModtimeScanningTest::SetUp()
{
AssetProcessorManagerTest::SetUp();
m_data = AZStd::make_unique<StaticData>();
// We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
m_mockApplicationManager->BusDisconnect();
m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc("test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}", { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) });
m_data->m_mockBuilderInfoHandler.BusConnect();
ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
// Run this twice so the test builder doesn't get counted as a "new" builder and bypass the modtime skipping
m_assetProcessorManager->ComputeBuilderDirty();
m_assetProcessorManager->ComputeBuilderDirty();
auto assetConnection = QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess, [this](JobDetails details)
{
m_data->m_processResults.push_back(AZStd::move(details));
});
auto deletedConnection = QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted, [this](QString file)
{
m_data->m_deletedSources.push_back(file);
});
// Create the test file
const auto& scanFolder = m_config->GetScanFolderAt(0);
m_data->m_relativePathFromWatchFolder[0] = "modtimeTestFile.txt";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[0]));
m_data->m_relativePathFromWatchFolder[1] = "modtimeTestDependency.txt";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[1]));
m_data->m_relativePathFromWatchFolder[2] = "modtimeTestDependency.txt.assetinfo";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[2]));
for (const auto& path : m_data->m_absolutePath)
{
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(path, ""));
}
m_data->m_mockBuilderInfoHandler.m_dependencyFilePath = m_data->m_absolutePath[1].toUtf8().data();
// Add file to database with no modtime
{
AssetDatabaseConnection connection;
ASSERT_TRUE(connection.OpenDatabase());
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[0].toUtf8().data();
fileEntry.m_modTime = 0;
fileEntry.m_isFolder = false;
fileEntry.m_scanFolderPK = scanFolder.ScanFolderID();
bool entryAlreadyExists;
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[1].toUtf8().data();
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[2].toUtf8().data();
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
}
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
ProcessAssetJobs();
m_data->m_processResults.clear();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_isIdling = false;
}
void ModtimeScanningTest::TearDown()
{
m_data = nullptr;
AssetProcessorManagerTest::TearDown();
}
void ModtimeScanningTest::ProcessAssetJobs()
{
m_data->m_productPaths.clear();
for (const auto& processResult : m_data->m_processResults)
{
auto file = QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1");
m_data->m_productPaths.emplace(
QDir(processResult.m_jobEntry.m_watchFolderPath)
.absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName)
.toUtf8()
.constData(),
file);
// Create the file on disk
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(file, "products."));
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file.toUtf8().constData(), AZ::Uuid::CreateNull(), 1));
QMetaObject::invokeMethod(m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry), Q_ARG(AssetBuilderSDK::ProcessJobResponse, response));
}
ASSERT_TRUE(BlockUntilIdle(5000));
m_isIdling = false;
}
void ModtimeScanningTest::SimulateAssetScanner(QSet<AssetFileInfo> filePaths)
{
QMetaObject::invokeMethod(m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection, Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Started));
QMetaObject::invokeMethod(m_assetProcessorManager.get(), "AssessFilesFromScanner", Qt::QueuedConnection, Q_ARG(QSet<AssetFileInfo>, filePaths));
QMetaObject::invokeMethod(m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection, Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Completed));
}
QSet<AssetFileInfo> ModtimeScanningTest::BuildFileSet()
{
QSet<AssetFileInfo> filePaths;
for (const auto& path : m_data->m_absolutePath)
{
QFileInfo fileInfo(path);
auto modtime = fileInfo.lastModified();
AZ::u64 fileSize = fileInfo.size();
filePaths.insert(AssetFileInfo(path, modtime, fileSize, m_config->GetScanFolderForFile(path), false));
}
return filePaths;
}
void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs)
{
ASSERT_TRUE(BlockUntilIdle(5000));
EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
EXPECT_EQ(m_data->m_processResults.size(), processJobs);
EXPECT_FALSE(m_data->m_processResults[0].m_autoFail);
EXPECT_FALSE(m_data->m_processResults[1].m_autoFail);
EXPECT_EQ(m_data->m_deletedSources.size(), 0);
m_isIdling = false;
}
void ModtimeScanningTest::ExpectNoWork()
{
// Since there's no work to do, the idle event isn't going to trigger, just process events a couple times
for (int i = 0; i < 10; ++i)
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
}
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
ASSERT_EQ(m_data->m_processResults.size(), 0);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
m_isIdling = false;
}
void ModtimeScanningTest::SetFileContents(QString filePath, QString contents)
{
QFile file(filePath);
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
file.write(contents.toUtf8().constData());
file.close();
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
{
using namespace AzToolsFramework::AssetSystem;
// Make sure modtime skipping is disabled
// We're just going to do 1 quick sanity test to make sure the files are still processed when modtime skipping is turned off
m_assetProcessorManager->m_allowModtimeSkippingFeature = false;
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// 2 create jobs but 0 process jobs because the file has already been processed before in SetUp
ExpectWork(2, 0);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged)
{
using namespace AzToolsFramework::AssetSystem;
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
{
using namespace AzToolsFramework::AssetSystem;
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
// Enable android platform after the initial SetUp has already processed the files for pc
QDir tempPath(m_tempDir.path());
AssetBuilderSDK::PlatformInfo androidPlatform("android", { "host", "renderer" });
m_config->EnablePlatform(androidPlatform, true);
// There's no way to remove scanfolders and adding a new one after enabling the platform will cause the pc assets to build as well, which we don't want
// Instead we'll just const cast the vector and modify the enabled platforms for the scanfolder
auto& platforms = const_cast<AZStd::vector<AssetBuilderSDK::PlatformInfo>&>(m_config->GetScanFolderAt(0).GetPlatforms());
platforms.push_back(androidPlatform);
// We need the builder fingerprints to be updated to reflect the newly enabled platform
m_assetProcessorManager->ComputeBuilderDirty();
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(4, 2); // CreateJobs = 4, 2 files * 2 platforms. ProcessJobs = 2, just the android platform jobs (pc is already processed)
ASSERT_TRUE(m_data->m_processResults[0].m_destinationPath.contains("android"));
ASSERT_TRUE(m_data->m_processResults[1].m_destinationPath.contains("android"));
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp)
{
// Update the timestamp on a file without changing its contents
// This should not cause any job to run since the hash of the file is the same before/after
// Additionally, the timestamp stored in the database should be updated
using namespace AzToolsFramework::AssetSystem;
uint64_t timestamp = 1594923423;
QString databaseName, scanfolderName;
m_config->ConvertToRelativePath(m_data->m_absolutePath[1], databaseName, scanfolderName);
auto* scanFolder = m_config->GetScanFolderForFile(m_data->m_absolutePath[1]);
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
m_assetProcessorManager.get()->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry);
ASSERT_NE(fileEntry.m_modTime, timestamp);
uint64_t existingTimestamp = fileEntry.m_modTime;
// Modify the timestamp on just one file
AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp);
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
m_assetProcessorManager.get()->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry);
// The timestamp should be updated even though nothing processed
ASSERT_NE(fileEntry.m_modTime, existingTimestamp);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile)
{
// Update the timestamp on a file without changing its contents
// This should not cause any job to run since the hash of the file is the same before/after
// Additionally, the timestamp stored in the database should be updated
using namespace AzToolsFramework::AssetSystem;
uint64_t timestamp = 1594923423;
// Modify the timestamp on just one file
AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp);
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, false);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world");
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers the other test file to process as well
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain)
{
using namespace AzToolsFramework::AssetSystem;
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
SetFileContents(theFileString, "hello world");
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers the other test file to process as well
ExpectWork(2, 2);
ProcessAssetJobs();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
SetFileContents(theFileString, "");
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Expect processing to happen again
ExpectWork(2, 2);
}
struct LockedFileTest
: ModtimeScanningTest
, AssetProcessor::ConnectionBus::Handler
{
MOCK_METHOD3(SendRaw, size_t (unsigned, unsigned, const QByteArray&));
MOCK_METHOD3(SendPerPlatform, size_t (unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const QString&));
MOCK_METHOD4(SendRawPerPlatform, size_t (unsigned, unsigned, const QByteArray&, const QString&));
MOCK_METHOD2(SendRequest, unsigned (const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const ResponseCallback&));
MOCK_METHOD2(SendResponse, size_t (unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&));
MOCK_METHOD1(RemoveResponseHandler, void (unsigned));
size_t Send(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override
{
using SourceFileNotificationMessage = AzToolsFramework::AssetSystem::SourceFileNotificationMessage;
switch (message.GetMessageType())
{
case SourceFileNotificationMessage::MessageType:
if (const auto sourceFileMessage = azrtti_cast<const SourceFileNotificationMessage*>(&message); sourceFileMessage != nullptr &&
sourceFileMessage->m_type == SourceFileNotificationMessage::NotificationType::FileRemoved)
{
// The File Remove message will occur before an attempt to delete the file
// Wait for more than 1 File Remove message.
// This indicates the AP has attempted to delete the file once, failed to do so and is now retrying
++m_deleteCounter;
if(m_deleteCounter > 1 && m_callback)
{
m_callback();
m_callback = {}; // Unset it to be safe, we only intend to run the callback once
}
}
break;
default:
break;
}
return 0;
}
void SetUp() override
{
ModtimeScanningTest::SetUp();
ConnectionBus::Handler::BusConnect(0);
}
void TearDown() override
{
ConnectionBus::Handler::BusDisconnect();
ModtimeScanningTest::TearDown();
}
AZStd::atomic_int m_deleteCounter{ 0 };
AZStd::function<void()> m_callback;
};
TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeleteFails)
{
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
{
QFile file(theFileString);
file.remove();
}
ASSERT_GT(m_data->m_productPaths.size(), 0);
QFile product(productPath);
ASSERT_TRUE(product.open(QIODevice::ReadOnly));
// Check if we can delete the file now, if we can't, proceed with the test
// If we can, it means the OS running this test doesn't lock open files so there's nothing to test
if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
{
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
EXPECT_TRUE(BlockUntilIdle(5000));
EXPECT_TRUE(QFile::exists(productPath));
EXPECT_EQ(m_data->m_deletedSources.size(), 0);
}
else
{
SUCCEED() << "Skipping test. OS does not lock open files.";
}
}
TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeletesWhenReleased)
{
// This test is intended to verify the AP will successfully retry deleting a source asset
// when one of its product assets is locked temporarily
// We'll lock the file by holding it open
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
{
QFile file(theFileString);
file.remove();
}
ASSERT_GT(m_data->m_productPaths.size(), 0);
QFile product(productPath);
// Open the file and keep it open to lock it
// We'll start a thread later to unlock the file
// This will allow us to test how AP handles trying to delete a locked file
ASSERT_TRUE(product.open(QIODevice::ReadOnly));
// Check if we can delete the file now, if we can't, proceed with the test
// If we can, it means the OS running this test doesn't lock open files so there's nothing to test
if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
{
m_deleteCounter = 0;
// Set up a callback which will fire after at least 1 retry
// Unlock the file at that point so AP can successfully delete it
m_callback = [&product]()
{
product.close();
};
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
EXPECT_TRUE(BlockUntilIdle(5000));
EXPECT_FALSE(QFile::exists(productPath));
EXPECT_EQ(m_data->m_deletedSources.size(), 1);
EXPECT_GT(m_deleteCounter, 1); // Make sure the AP tried more than once to delete the file
m_errorAbsorber->ExpectAsserts(0);
}
else
{
SUCCEED() << "Skipping test. OS does not lock open files.";
}
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world");
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers the other test file to process as well
ExpectWork(2, 2);
ProcessAssetJobs();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
// Make file 0 have the same contents as file 1
SetFileContents(m_data->m_absolutePath[0].toUtf8().constData(), "hello world");
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(1, 1);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[2].toUtf8().constData(), "hello world");
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a metadata file
// that triggers the source file which is a dependency that triggers the other test file to process as well
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_DeleteFile)
{
using namespace AzToolsFramework::AssetSystem;
// Enable the features we're testing
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
AssetUtilities::SetUseFileHashOverride(true, true);
ASSERT_TRUE(QFile::remove(m_data->m_absolutePath[0]));
// Feed in ONLY one file (the one we didn't delete)
QSet<AssetFileInfo> filePaths;
QFileInfo fileInfo(m_data->m_absolutePath[1]);
auto modtime = fileInfo.lastModified();
AZ::u64 fileSize = fileInfo.size();
filePaths.insert(AssetFileInfo(m_data->m_absolutePath[1], modtime, fileSize, &m_config->GetScanFolderAt(0), false));
SimulateAssetScanner(filePaths);
QElapsedTimer timer;
timer.start();
do
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
} while (m_data->m_deletedSources.size() < m_data->m_relativePathFromWatchFolder[0].size() && timer.elapsed() < 5000);
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
ASSERT_EQ(m_data->m_processResults.size(), 0);
ASSERT_THAT(m_data->m_deletedSources, testing::ElementsAre(m_data->m_relativePathFromWatchFolder[0]));
}
TEST_F(ModtimeScanningTest, ReprocessRequest_FileNotModified_FileProcessed)
{
using namespace AzToolsFramework::AssetSystem;
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
ASSERT_EQ(m_data->m_processResults.size(), 1);
}
TEST_F(ModtimeScanningTest, ReprocessRequest_SourceWithDependency_BothWillProcess)
{
using namespace AzToolsFramework::AssetSystem;
using SourceFileDependencyEntry = AzToolsFramework::AssetDatabase::SourceFileDependencyEntry;
SourceFileDependencyEntry newEntry1;
newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
newEntry1.m_source = m_data->m_absolutePath[0].toUtf8().constData();
newEntry1.m_dependsOnSource = m_data->m_absolutePath[1].toUtf8().constData();
newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource;
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
ASSERT_EQ(m_data->m_processResults.size(), 1);
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[1]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 3);
ASSERT_EQ(m_data->m_processResults.size(), 3);
}
TEST_F(ModtimeScanningTest, ReprocessRequest_RequestFolder_SourceAssetsWillProcess)
{
using namespace AzToolsFramework::AssetSystem;
const auto& scanFolder = m_config->GetScanFolderAt(0);
QString scanPath = scanFolder.ScanPath();
m_assetProcessorManager->RequestReprocess(scanPath);
ASSERT_TRUE(BlockUntilIdle(5000));
// two text files are source assets, assetinfo is not
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
}
//////////////////////////////////////////////////////////////////////////
MockBuilderInfoHandler::~MockBuilderInfoHandler()
@@ -5205,130 +4477,7 @@ TEST_F(ChainJobDependencyTest, TestChainDependency_Multi)
}
}
void DeleteTest::SetUp()
{
AssetProcessorManagerTest::SetUp();
m_data = AZStd::make_unique<StaticData>();
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
// We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
m_mockApplicationManager->BusDisconnect();
m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc("test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}", { AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) });
m_data->m_mockBuilderInfoHandler.BusConnect();
ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
// Run this twice so the test builder doesn't get counted as a "new" builder and bypass the modtime skipping
m_assetProcessorManager->ComputeBuilderDirty();
m_assetProcessorManager->ComputeBuilderDirty();
auto setupConnectionsFunc = [this]()
{
QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess, [this](JobDetails details)
{
m_data->m_processResults.push_back(AZStd::move(details));
});
QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted, [this](QString file)
{
m_data->m_deletedSources.push_back(file);
});
};
auto createFileAndAddToDatabaseFunc = [this](const AssetProcessor::ScanFolderInfo* scanFolder, QString file)
{
using namespace AzToolsFramework::AssetDatabase;
QString watchFolderPath = scanFolder->ScanPath();
QString absPath(QDir(watchFolderPath).absoluteFilePath(file));
UnitTestUtils::CreateDummyFile(absPath);
m_data->m_absolutePath.push_back(absPath);
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
fileEntry.m_fileName = file.toUtf8().constData();
fileEntry.m_modTime = 0;
fileEntry.m_isFolder = false;
fileEntry.m_scanFolderPK = scanFolder->ScanFolderID();
bool entryAlreadyExists;
ASSERT_TRUE(m_assetProcessorManager->m_stateData->InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
};
setupConnectionsFunc();
// Create test files
QDir tempPath(m_tempDir.path());
const auto* scanFolder1 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder1"));
const auto* scanFolder4 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder4"));
createFileAndAddToDatabaseFunc(scanFolder1, QString("textures/a.txt"));
createFileAndAddToDatabaseFunc(scanFolder4, QString("textures/b.txt"));
// Run the test files through AP all the way to processing stage
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
ProcessAssetJobs();
m_data->m_processResults.clear();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
// Reboot the APM since we added stuff to the database that needs to be loaded on-startup of the APM
m_assetProcessorManager.reset(new AssetProcessorManager_Test(m_config.get()));
m_idleConnection = QObject::connect(m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::AssetProcessorManagerIdleState, [this](bool newState)
{
m_isIdling = newState;
});
setupConnectionsFunc();
m_assetProcessorManager->ComputeBuilderDirty();
}
TEST_F(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache)
{
// There was a bug where AP wasn't repopulating the "known folders" list when modtime skipping was enabled and no work was needed
// As a result, deleting a folder didn't count as a "folder", so the wrong code path was taken. This test makes sure the correct deletion events fire
using namespace AzToolsFramework::AssetSystem;
// Modtime skipping has to be on for this
m_assetProcessorManager->m_allowModtimeSkippingFeature = true;
// Feed in the files from the asset scanner, no jobs should run since they're already up-to-date
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
// Delete one of the folders
QDir tempPath(m_tempDir.path());
QString absPath(tempPath.absoluteFilePath("subfolder1/textures"));
QDir(absPath).removeRecursively();
AZStd::vector<AZStd::string> deletedFolders;
QObject::connect(m_assetProcessorManager.get(), &AssetProcessorManager::SourceFolderDeleted, [&deletedFolders](QString file)
{
deletedFolders.push_back(file.toUtf8().constData());
});
m_assetProcessorManager->AssessDeletedFile(absPath);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_THAT(m_data->m_deletedSources, testing::UnorderedElementsAre("textures/a.txt"));
ASSERT_THAT(deletedFolders, testing::UnorderedElementsAre("textures"));
}
void DuplicateProcessTest::SetUp()
{
@@ -37,6 +37,114 @@ public:
MOCK_METHOD1(GetAssetDatabaseLocation, bool(AZStd::string&));
};
class AssetProcessorManager_Test : public AssetProcessor::AssetProcessorManager
{
public:
friend class GTEST_TEST_CLASS_NAME_(
AssetProcessorManagerTest, AssetProcessedImpl_DifferentProductDependenciesPerProduct_SavesCorrectlyToDatabase);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_DeferredResolution);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, SameFilenameForAllPlatforms);
friend class GTEST_TEST_CLASS_NAME_(MultiplatformPathDependencyTest, AssetProcessed_Impl_MultiplatformDependencies_SourcePath);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, DeleteFolder_SignalsDeleteOfContainedFiles);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_WithDifferentTypes_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_Reverse_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(
AssetProcessorManagerTest, QueryAbsolutePathDependenciesRecursive_MissingFiles_ReturnsNoPathWithPlaceholders);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_BeforeComputingDirtiness_AllDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_EmptyDatabase_AllDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_SameAsLastTime_NoneDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_MoreThanLastTime_NewOneIsDirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_FewerThanLastTime_Dirty);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPattern_CountsAsNew);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_ChangedPatternType_CountsAsNew);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewPattern_CountsAsNewBuilder);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewVersionNumber_IsNotANewBuilder);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, BuilderDirtiness_NewAnalysisFingerprint_IsNotANewBuilder);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_BasicTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_UpdateTest);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName);
friend class GTEST_TEST_CLASS_NAME_(
AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByUuid_UpdatesWhenTheyAppear);
friend class GTEST_TEST_CLASS_NAME_(
AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_MissingFiles_ByName_UpdatesWhenTheyAppear);
friend class GTEST_TEST_CLASS_NAME_(
AssetProcessorManagerTest, UpdateSourceFileDependenciesDatabase_WildcardMissingFiles_ByName_UpdatesWhenTheyAppear);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, JobDependencyOrderOnce_MultipleJobs_EmitOK);
friend class GTEST_TEST_CLASS_NAME_(AssetProcessorManagerTest, SourceFileProcessFailure_ClearsFingerprint);
friend class GTEST_TEST_CLASS_NAME_(
AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_DoesNotDuplicateDependency);
friend class GTEST_TEST_CLASS_NAME_(
AbsolutePathProductDependencyTest, AbsolutePathProductDependency_RetryDeferredDependenciesWithMatchingSource_DependencyResolves);
friend class GTEST_TEST_CLASS_NAME_(
AbsolutePathProductDependencyTest, UnresolvedProductPathDependency_AssetProcessedTwice_ValidatePathDependenciesMap);
friend class GTEST_TEST_CLASS_NAME_(
AbsolutePathProductDependencyTest,
UnresolvedSourceFileTypeProductPathDependency_DependencyHasNoProductOutput_ValidatePathDependenciesMap);
friend class GTEST_TEST_CLASS_NAME_(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache);
friend class GTEST_TEST_CLASS_NAME_(MetadataFileTest, MetadataFile_SourceFileExtensionDifferentCase);
friend class AssetProcessorManagerTest;
friend struct JobDependencyTest;
friend struct ChainJobDependencyTest;
friend struct DeleteTest;
friend struct PathDependencyTest;
friend struct DuplicateProductsTest;
friend struct DuplicateProcessTest;
friend struct AbsolutePathProductDependencyTest;
friend struct WildcardSourceDependencyTest;
explicit AssetProcessorManager_Test(AssetProcessor::PlatformConfiguration* config, QObject* parent = nullptr);
~AssetProcessorManager_Test() override;
bool CheckJobKeyToJobRunKeyMap(AZStd::string jobKey);
int CountDirtyBuilders() const
{
int numDirty = 0;
for (const auto& element : m_builderDataCache)
{
if (element.second.m_isDirty)
{
++numDirty;
}
}
return numDirty;
}
bool IsBuilderDirty(const AZ::Uuid& builderBusId) const
{
auto finder = m_builderDataCache.find(builderBusId);
if (finder == m_builderDataCache.end())
{
return true;
}
return finder->second.m_isDirty;
}
void RecomputeDirtyBuilders()
{
// Run this twice so the test builder doesn't get counted as a "new" builder and bypass the modtime skipping
ComputeBuilderDirty();
ComputeBuilderDirty();
}
using AssetProcessorManager::m_stateData;
using AssetProcessorManager::ComputeBuilderDirty;
};
class AssetProcessorManagerTest
: public AssetProcessor::AssetProcessorTest
{
@@ -165,33 +273,6 @@ struct MockBuilderInfoHandler
int m_createJobsCount = 0;
};
struct ModtimeScanningTest
: public AssetProcessorManagerTest
{
void SetUp() override;
void TearDown() override;
void ProcessAssetJobs();
void SimulateAssetScanner(QSet<AssetProcessor::AssetFileInfo> filePaths);
QSet<AssetProcessor::AssetFileInfo> BuildFileSet();
void ExpectWork(int createJobs, int processJobs);
void ExpectNoWork();
void SetFileContents(QString filePath, QString contents);
struct StaticData
{
QString m_relativePathFromWatchFolder[3];
AZStd::vector<QString> m_absolutePath;
AZStd::vector<AssetProcessor::JobDetails> m_processResults;
AZStd::unordered_multimap<AZStd::string, QString> m_productPaths;
AZStd::vector<QString> m_deletedSources;
AZStd::shared_ptr<AssetProcessor::InternalMockBuilder> m_builderTxtBuilder;
MockBuilderInfoHandler m_mockBuilderInfoHandler;
};
AZStd::unique_ptr<StaticData> m_data;
};
struct MetadataFileTest
: public AssetProcessorManagerTest
@@ -274,9 +355,3 @@ struct DuplicateProductsTest
{
void SetupDuplicateProductsTest(QString& sourceFile, QDir& tempPath, QString& productFile, AZStd::vector<AssetProcessor::JobDetails>& jobDetails, AssetBuilderSDK::ProcessJobResponse& response, bool multipleOutputs, QString extension);
};
struct DeleteTest
: public ModtimeScanningTest
{
void SetUp() override;
};
@@ -0,0 +1,706 @@
/*
* 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 <native/tests/assetmanager/ModtimeScanningTests.h>
#include <native/tests/assetmanager/AssetProcessorManagerTest.h>
#include <QObject>
#include <ToolsFileUtils/ToolsFileUtils.h>
namespace UnitTests
{
using AssetFileInfo = AssetProcessor::AssetFileInfo;
void ModtimeScanningTest::SetUpAssetProcessorManager()
{
using namespace AssetProcessor;
m_assetProcessorManager->SetEnableModtimeSkippingFeature(true);
m_assetProcessorManager->RecomputeDirtyBuilders();
QObject::connect(
m_assetProcessorManager.get(), &AssetProcessorManager::AssetToProcess,
[this](JobDetails details)
{
m_data->m_processResults.push_back(AZStd::move(details));
});
QObject::connect(
m_assetProcessorManager.get(), &AssetProcessorManager::SourceDeleted,
[this](QString file)
{
m_data->m_deletedSources.push_back(file);
});
m_idleConnection = QObject::connect(
m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::AssetProcessorManagerIdleState,
[this](bool newState)
{
m_isIdling = newState;
});
}
void ModtimeScanningTest::SetUp()
{
using namespace AssetProcessor;
AssetProcessorManagerTest::SetUp();
m_data = AZStd::make_unique<StaticData>();
// We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
m_mockApplicationManager->BusDisconnect();
m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc(
"test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}",
{ AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) });
m_data->m_mockBuilderInfoHandler.BusConnect();
ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
SetUpAssetProcessorManager();
// Create the test file
const auto& scanFolder = m_config->GetScanFolderAt(0);
m_data->m_relativePathFromWatchFolder[0] = "modtimeTestFile.txt";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[0]));
m_data->m_relativePathFromWatchFolder[1] = "modtimeTestDependency.txt";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[1]));
m_data->m_relativePathFromWatchFolder[2] = "modtimeTestDependency.txt.assetinfo";
m_data->m_absolutePath.push_back(QDir(scanFolder.ScanPath()).absoluteFilePath(m_data->m_relativePathFromWatchFolder[2]));
for (const auto& path : m_data->m_absolutePath)
{
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(path, ""));
}
m_data->m_mockBuilderInfoHandler.m_dependencyFilePath = m_data->m_absolutePath[1].toUtf8().data();
// Add file to database with no modtime
{
AssetDatabaseConnection connection;
ASSERT_TRUE(connection.OpenDatabase());
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[0].toUtf8().data();
fileEntry.m_modTime = 0;
fileEntry.m_isFolder = false;
fileEntry.m_scanFolderPK = scanFolder.ScanFolderID();
bool entryAlreadyExists;
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[1].toUtf8().data();
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
fileEntry.m_fileID = AzToolsFramework::AssetDatabase::InvalidEntryId; // Reset the id so we make a new entry
fileEntry.m_fileName = m_data->m_relativePathFromWatchFolder[2].toUtf8().data();
ASSERT_TRUE(connection.InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
}
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
ProcessAssetJobs();
m_data->m_processResults.clear();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_isIdling = false;
}
void ModtimeScanningTest::TearDown()
{
m_data = nullptr;
AssetProcessorManagerTest::TearDown();
}
void ModtimeScanningTest::ProcessAssetJobs()
{
m_data->m_productPaths.clear();
for (const auto& processResult : m_data->m_processResults)
{
auto file =
QDir(processResult.m_destinationPath).absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName.toLower() + ".arc1");
m_data->m_productPaths.emplace(
QDir(processResult.m_jobEntry.m_watchFolderPath)
.absoluteFilePath(processResult.m_jobEntry.m_databaseSourceName)
.toUtf8()
.constData(),
file);
// Create the file on disk
ASSERT_TRUE(UnitTestUtils::CreateDummyFile(file, "products."));
AssetBuilderSDK::ProcessJobResponse response;
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
response.m_outputProducts.push_back(AssetBuilderSDK::JobProduct(file.toUtf8().constData(), AZ::Uuid::CreateNull(), 1));
using JobEntry = AssetProcessor::JobEntry;
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssetProcessed", Qt::QueuedConnection, Q_ARG(JobEntry, processResult.m_jobEntry),
Q_ARG(AssetBuilderSDK::ProcessJobResponse, response));
}
ASSERT_TRUE(BlockUntilIdle(5000));
m_isIdling = false;
}
void ModtimeScanningTest::SimulateAssetScanner(QSet<AssetProcessor::AssetFileInfo> filePaths)
{
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection,
Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Started));
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessFilesFromScanner", Qt::QueuedConnection, Q_ARG(QSet<AssetFileInfo>, filePaths));
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "OnAssetScannerStatusChange", Qt::QueuedConnection,
Q_ARG(AssetProcessor::AssetScanningStatus, AssetProcessor::AssetScanningStatus::Completed));
}
QSet<AssetProcessor::AssetFileInfo> ModtimeScanningTest::BuildFileSet()
{
QSet<AssetFileInfo> filePaths;
for (const auto& path : m_data->m_absolutePath)
{
QFileInfo fileInfo(path);
auto modtime = fileInfo.lastModified();
AZ::u64 fileSize = fileInfo.size();
filePaths.insert(AssetFileInfo(path, modtime, fileSize, m_config->GetScanFolderForFile(path), false));
}
return filePaths;
}
void ModtimeScanningTest::ExpectWork(int createJobs, int processJobs)
{
ASSERT_TRUE(BlockUntilIdle(5000));
EXPECT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, createJobs);
EXPECT_EQ(m_data->m_processResults.size(), processJobs);
for (int i = 0; i < processJobs; ++i)
{
EXPECT_FALSE(m_data->m_processResults[i].m_autoFail);
}
EXPECT_EQ(m_data->m_deletedSources.size(), 0);
m_isIdling = false;
}
void ModtimeScanningTest::ExpectNoWork()
{
// Since there's no work to do, the idle event isn't going to trigger, just process events a couple times
for (int i = 0; i < 10; ++i)
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
}
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
ASSERT_EQ(m_data->m_processResults.size(), 0);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
m_isIdling = false;
}
void ModtimeScanningTest::SetFileContents(QString filePath, QString contents)
{
QFile file(filePath);
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
file.write(contents.toUtf8().constData());
file.close();
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged_WithoutModtimeSkipping)
{
using namespace AzToolsFramework::AssetSystem;
// Make sure modtime skipping is disabled
// We're just going to do 1 quick sanity test to make sure the files are still processed when modtime skipping is turned off
m_assetProcessorManager->SetEnableModtimeSkippingFeature(false);
QSet<AssetProcessor::AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// 2 create jobs but 0 process jobs because the file has already been processed before in SetUp
ExpectWork(2, 0);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_FileUnchanged)
{
using namespace AzToolsFramework::AssetSystem;
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_EnablePlatform_ShouldProcessFilesForPlatform)
{
using namespace AzToolsFramework::AssetSystem;
AssetUtilities::SetUseFileHashOverride(true, true);
// Enable android platform after the initial SetUp has already processed the files for pc
QDir tempPath(m_tempDir.path());
AssetBuilderSDK::PlatformInfo androidPlatform("android", { "host", "renderer" });
m_config->EnablePlatform(androidPlatform, true);
// There's no way to remove scanfolders and adding a new one after enabling the platform will cause the pc assets to build as well,
// which we don't want Instead we'll just const cast the vector and modify the enabled platforms for the scanfolder
auto& platforms = const_cast<AZStd::vector<AssetBuilderSDK::PlatformInfo>&>(m_config->GetScanFolderAt(0).GetPlatforms());
platforms.push_back(androidPlatform);
// We need the builder fingerprints to be updated to reflect the newly enabled platform
m_assetProcessorManager->ComputeBuilderDirty();
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(
4, 2); // CreateJobs = 4, 2 files * 2 platforms. ProcessJobs = 2, just the android platform jobs (pc is already processed)
ASSERT_TRUE(m_data->m_processResults[0].m_destinationPath.contains("android"));
ASSERT_TRUE(m_data->m_processResults[1].m_destinationPath.contains("android"));
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestamp)
{
// Update the timestamp on a file without changing its contents
// This should not cause any job to run since the hash of the file is the same before/after
// Additionally, the timestamp stored in the database should be updated
using namespace AzToolsFramework::AssetSystem;
uint64_t timestamp = 1594923423;
QString databaseName, scanfolderName;
m_config->ConvertToRelativePath(m_data->m_absolutePath[1], databaseName, scanfolderName);
auto* scanFolder = m_config->GetScanFolderForFile(m_data->m_absolutePath[1]);
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry);
ASSERT_NE(fileEntry.m_modTime, timestamp);
uint64_t existingTimestamp = fileEntry.m_modTime;
// Modify the timestamp on just one file
AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp);
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
m_assetProcessorManager->m_stateData->GetFileByFileNameAndScanFolderId(databaseName, scanFolder->ScanFolderID(), fileEntry);
// The timestamp should be updated even though nothing processed
ASSERT_NE(fileEntry.m_modTime, existingTimestamp);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyTimestampNoHashing_ProcessesFile)
{
// Update the timestamp on a file without changing its contents
// This should not cause any job to run since the hash of the file is the same before/after
// Additionally, the timestamp stored in the database should be updated
using namespace AzToolsFramework::AssetSystem;
uint64_t timestamp = 1594923423;
// Modify the timestamp on just one file
AzToolsFramework::ToolsFileUtils::SetModificationTime(m_data->m_absolutePath[1].toUtf8().data(), timestamp);
AssetUtilities::SetUseFileHashOverride(true, false);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFile_AndThenRevert_ProcessesAgain)
{
using namespace AzToolsFramework::AssetSystem;
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
SetFileContents(theFileString, "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
ProcessAssetJobs();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
SetFileContents(theFileString, "");
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Expect processing to happen again
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyFilesSameHash_BothProcess)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[1].toUtf8().constData(), "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a dependency that triggers
// the other test file to process as well
ExpectWork(2, 2);
ProcessAssetJobs();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
m_data->m_processResults.clear();
m_data->m_deletedSources.clear();
// Make file 0 have the same contents as file 1
SetFileContents(m_data->m_absolutePath[0].toUtf8().constData(), "hello world");
filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectWork(1, 1);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_ModifyMetadataFile)
{
using namespace AzToolsFramework::AssetSystem;
SetFileContents(m_data->m_absolutePath[2].toUtf8().constData(), "hello world");
AssetUtilities::SetUseFileHashOverride(true, true);
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
// Even though we're only updating one file, we're expecting 2 createJob calls because our test file is a metadata file
// that triggers the source file which is a dependency that triggers the other test file to process as well
ExpectWork(2, 2);
}
TEST_F(ModtimeScanningTest, ModtimeSkipping_DeleteFile)
{
using namespace AzToolsFramework::AssetSystem;
AssetUtilities::SetUseFileHashOverride(true, true);
ASSERT_TRUE(QFile::remove(m_data->m_absolutePath[0]));
// Feed in ONLY one file (the one we didn't delete)
QSet<AssetFileInfo> filePaths;
QFileInfo fileInfo(m_data->m_absolutePath[1]);
auto modtime = fileInfo.lastModified();
AZ::u64 fileSize = fileInfo.size();
filePaths.insert(AssetFileInfo(m_data->m_absolutePath[1], modtime, fileSize, &m_config->GetScanFolderAt(0), false));
SimulateAssetScanner(filePaths);
QElapsedTimer timer;
timer.start();
do
{
QCoreApplication::processEvents(QEventLoop::AllEvents, 10);
} while (m_data->m_deletedSources.size() < m_data->m_relativePathFromWatchFolder[0].size() && timer.elapsed() < 5000);
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 0);
ASSERT_EQ(m_data->m_processResults.size(), 0);
ASSERT_THAT(m_data->m_deletedSources, testing::ElementsAre(m_data->m_relativePathFromWatchFolder[0]));
}
TEST_F(ModtimeScanningTest, ReprocessRequest_FileNotModified_FileProcessed)
{
using namespace AzToolsFramework::AssetSystem;
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
ASSERT_EQ(m_data->m_processResults.size(), 1);
}
TEST_F(ModtimeScanningTest, ReprocessRequest_SourceWithDependency_BothWillProcess)
{
using namespace AzToolsFramework::AssetSystem;
using SourceFileDependencyEntry = AzToolsFramework::AssetDatabase::SourceFileDependencyEntry;
SourceFileDependencyEntry newEntry1;
newEntry1.m_sourceDependencyID = AzToolsFramework::AssetDatabase::InvalidEntryId;
newEntry1.m_builderGuid = AZ::Uuid::CreateRandom();
newEntry1.m_source = m_data->m_absolutePath[0].toUtf8().constData();
newEntry1.m_dependsOnSource = m_data->m_absolutePath[1].toUtf8().constData();
newEntry1.m_typeOfDependency = SourceFileDependencyEntry::DEP_SourceToSource;
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[0]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 1);
ASSERT_EQ(m_data->m_processResults.size(), 1);
m_assetProcessorManager->RequestReprocess(m_data->m_absolutePath[1]);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 3);
ASSERT_EQ(m_data->m_processResults.size(), 3);
}
TEST_F(ModtimeScanningTest, ReprocessRequest_RequestFolder_SourceAssetsWillProcess)
{
using namespace AzToolsFramework::AssetSystem;
const auto& scanFolder = m_config->GetScanFolderAt(0);
QString scanPath = scanFolder.ScanPath();
m_assetProcessorManager->RequestReprocess(scanPath);
ASSERT_TRUE(BlockUntilIdle(5000));
// two text files are source assets, assetinfo is not
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
}
void DeleteTest::SetUp()
{
AssetProcessorManagerTest::SetUp();
m_data = AZStd::make_unique<StaticData>();
// We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own
m_mockApplicationManager->BusDisconnect();
m_data->m_mockBuilderInfoHandler.m_builderDesc = m_data->m_mockBuilderInfoHandler.CreateBuilderDesc(
"test builder", "{DF09DDC0-FD22-43B6-9E22-22C8574A6E1E}",
{ AssetBuilderSDK::AssetBuilderPattern("*.txt", AssetBuilderSDK::AssetBuilderPattern::Wildcard) });
m_data->m_mockBuilderInfoHandler.BusConnect();
ASSERT_TRUE(m_mockApplicationManager->GetBuilderByID("txt files", m_data->m_builderTxtBuilder));
SetUpAssetProcessorManager();
auto createFileAndAddToDatabaseFunc = [this](const AssetProcessor::ScanFolderInfo* scanFolder, QString file)
{
using namespace AzToolsFramework::AssetDatabase;
QString watchFolderPath = scanFolder->ScanPath();
QString absPath(QDir(watchFolderPath).absoluteFilePath(file));
UnitTestUtils::CreateDummyFile(absPath);
m_data->m_absolutePath.push_back(absPath);
AzToolsFramework::AssetDatabase::FileDatabaseEntry fileEntry;
fileEntry.m_fileName = file.toUtf8().constData();
fileEntry.m_modTime = 0;
fileEntry.m_isFolder = false;
fileEntry.m_scanFolderPK = scanFolder->ScanFolderID();
bool entryAlreadyExists;
ASSERT_TRUE(m_assetProcessorManager->m_stateData->InsertFile(fileEntry, entryAlreadyExists));
ASSERT_FALSE(entryAlreadyExists);
};
// Create test files
QDir tempPath(m_tempDir.path());
const auto* scanFolder1 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder1"));
const auto* scanFolder4 = m_config->GetScanFolderByPath(tempPath.absoluteFilePath("subfolder4"));
createFileAndAddToDatabaseFunc(scanFolder1, QString("textures/a.txt"));
createFileAndAddToDatabaseFunc(scanFolder4, QString("textures/b.txt"));
// Run the test files through AP all the way to processing stage
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_EQ(m_data->m_mockBuilderInfoHandler.m_createJobsCount, 2);
ASSERT_EQ(m_data->m_processResults.size(), 2);
ASSERT_EQ(m_data->m_deletedSources.size(), 0);
ProcessAssetJobs();
m_data->m_processResults.clear();
m_data->m_mockBuilderInfoHandler.m_createJobsCount = 0;
// Reboot the APM since we added stuff to the database that needs to be loaded on-startup of the APM
m_assetProcessorManager.reset(new AssetProcessorManager_Test(m_config.get()));
SetUpAssetProcessorManager();
}
TEST_F(DeleteTest, DeleteFolderSharedAcrossTwoScanFolders_CorrectFileAndFolderAreDeletedFromCache)
{
// There was a bug where AP wasn't repopulating the "known folders" list when modtime skipping was enabled and no work was needed
// As a result, deleting a folder didn't count as a "folder", so the wrong code path was taken. This test makes sure the correct
// deletion events fire
using namespace AzToolsFramework::AssetSystem;
// Feed in the files from the asset scanner, no jobs should run since they're already up-to-date
QSet<AssetFileInfo> filePaths = BuildFileSet();
SimulateAssetScanner(filePaths);
ExpectNoWork();
// Delete one of the folders
QDir tempPath(m_tempDir.path());
QString absPath(tempPath.absoluteFilePath("subfolder1/textures"));
QDir(absPath).removeRecursively();
AZStd::vector<AZStd::string> deletedFolders;
QObject::connect(
m_assetProcessorManager.get(), &AssetProcessor::AssetProcessorManager::SourceFolderDeleted,
[&deletedFolders](QString file)
{
deletedFolders.push_back(file.toUtf8().constData());
});
m_assetProcessorManager->AssessDeletedFile(absPath);
ASSERT_TRUE(BlockUntilIdle(5000));
ASSERT_THAT(m_data->m_deletedSources, testing::UnorderedElementsAre("textures/a.txt"));
ASSERT_THAT(deletedFolders, testing::UnorderedElementsAre("textures"));
}
TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeleteFails)
{
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
{
QFile file(theFileString);
file.remove();
}
ASSERT_GT(m_data->m_productPaths.size(), 0);
QFile product(productPath);
ASSERT_TRUE(product.open(QIODevice::ReadOnly));
// Check if we can delete the file now, if we can't, proceed with the test
// If we can, it means the OS running this test doesn't lock open files so there's nothing to test
if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
{
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
EXPECT_TRUE(BlockUntilIdle(5000));
EXPECT_TRUE(QFile::exists(productPath));
EXPECT_EQ(m_data->m_deletedSources.size(), 0);
}
else
{
SUCCEED() << "Skipping test. OS does not lock open files.";
}
}
TEST_F(LockedFileTest, DeleteFile_LockedProduct_DeletesWhenReleased)
{
// This test is intended to verify the AP will successfully retry deleting a source asset
// when one of its product assets is locked temporarily
// We'll lock the file by holding it open
auto theFile = m_data->m_absolutePath[1].toUtf8();
const char* theFileString = theFile.constData();
auto [sourcePath, productPath] = *m_data->m_productPaths.find(theFileString);
{
QFile file(theFileString);
file.remove();
}
ASSERT_GT(m_data->m_productPaths.size(), 0);
QFile product(productPath);
// Open the file and keep it open to lock it
// We'll start a thread later to unlock the file
// This will allow us to test how AP handles trying to delete a locked file
ASSERT_TRUE(product.open(QIODevice::ReadOnly));
// Check if we can delete the file now, if we can't, proceed with the test
// If we can, it means the OS running this test doesn't lock open files so there's nothing to test
if (!AZ::IO::SystemFile::Delete(productPath.toUtf8().constData()))
{
m_deleteCounter = 0;
// Set up a callback which will fire after at least 1 retry
// Unlock the file at that point so AP can successfully delete it
m_callback = [&product]()
{
product.close();
};
QMetaObject::invokeMethod(
m_assetProcessorManager.get(), "AssessDeletedFile", Qt::QueuedConnection, Q_ARG(QString, QString(theFileString)));
EXPECT_TRUE(BlockUntilIdle(5000));
EXPECT_FALSE(QFile::exists(productPath));
EXPECT_EQ(m_data->m_deletedSources.size(), 1);
EXPECT_GT(m_deleteCounter, 1); // Make sure the AP tried more than once to delete the file
m_errorAbsorber->ExpectAsserts(0);
}
else
{
SUCCEED() << "Skipping test. OS does not lock open files.";
}
}
}
@@ -0,0 +1,105 @@
/*
* 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 <tests/assetmanager/AssetProcessorManagerTest.h>
namespace UnitTests
{
struct ModtimeScanningTest : AssetProcessorManagerTest
{
void SetUpAssetProcessorManager();
void SetUp() override;
void TearDown() override;
void ProcessAssetJobs();
void SimulateAssetScanner(QSet<AssetProcessor::AssetFileInfo> filePaths);
QSet<AssetProcessor::AssetFileInfo> BuildFileSet();
void ExpectWork(int createJobs, int processJobs);
void ExpectNoWork();
void SetFileContents(QString filePath, QString contents);
struct StaticData
{
QString m_relativePathFromWatchFolder[3];
AZStd::vector<QString> m_absolutePath;
AZStd::vector<AssetProcessor::JobDetails> m_processResults;
AZStd::unordered_multimap<AZStd::string, QString> m_productPaths;
AZStd::vector<QString> m_deletedSources;
AZStd::shared_ptr<AssetProcessor::InternalMockBuilder> m_builderTxtBuilder;
MockBuilderInfoHandler m_mockBuilderInfoHandler;
};
AZStd::unique_ptr<StaticData> m_data;
};
struct DeleteTest : ModtimeScanningTest
{
void SetUp() override;
};
struct LockedFileTest
: ModtimeScanningTest
, AssetProcessor::ConnectionBus::Handler
{
MOCK_METHOD3(SendRaw, size_t(unsigned, unsigned, const QByteArray&));
MOCK_METHOD3(SendPerPlatform, size_t(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const QString&));
MOCK_METHOD4(SendRawPerPlatform, size_t(unsigned, unsigned, const QByteArray&, const QString&));
MOCK_METHOD2(SendRequest, unsigned(const AzFramework::AssetSystem::BaseAssetProcessorMessage&, const ResponseCallback&));
MOCK_METHOD2(SendResponse, size_t(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage&));
MOCK_METHOD1(RemoveResponseHandler, void(unsigned));
size_t Send(unsigned, const AzFramework::AssetSystem::BaseAssetProcessorMessage& message) override
{
using SourceFileNotificationMessage = AzToolsFramework::AssetSystem::SourceFileNotificationMessage;
switch (message.GetMessageType())
{
case SourceFileNotificationMessage::MessageType:
if (const auto sourceFileMessage = azrtti_cast<const SourceFileNotificationMessage*>(&message);
sourceFileMessage != nullptr &&
sourceFileMessage->m_type == SourceFileNotificationMessage::NotificationType::FileRemoved)
{
// The File Remove message will occur before an attempt to delete the file
// Wait for more than 1 File Remove message.
// This indicates the AP has attempted to delete the file once, failed to do so and is now retrying
++m_deleteCounter;
if (m_deleteCounter > 1 && m_callback)
{
m_callback();
m_callback = {}; // Unset it to be safe, we only intend to run the callback once
}
}
break;
default:
break;
}
return 0;
}
void SetUp() override
{
ModtimeScanningTest::SetUp();
AssetProcessor::ConnectionBus::Handler::BusConnect(0);
}
void TearDown() override
{
AssetProcessor::ConnectionBus::Handler::BusDisconnect();
ModtimeScanningTest::TearDown();
}
AZStd::atomic_int m_deleteCounter{ 0 };
AZStd::function<void()> m_callback;
};
}
@@ -500,6 +500,10 @@ QProgressBar::chunk {
/************** Gem Catalog **************/
#GemCatalogScreen {
background-color: #333333;
}
#GemCatalogTitle {
font-size: 18px;
}
@@ -546,9 +550,8 @@ QProgressBar::chunk {
min-height:24px;
}
#GemCatalogHeaderLabel {
font-size: 12px;
color: #FFFFFF;
#adjustableHeaderWidget QHeaderView::section {
background-color: transparent;
}
#GemCatalogHeaderShowCountLabel {
@@ -732,15 +735,6 @@ QProgressBar::chunk {
stop: 0 #555555, stop: 1.0 #777777);
}
#gemRepoHeaderTable {
background-color: transparent;
max-height: 30px;
}
#gemRepoListHeader {
background-color: transparent;
}
#gemRepoInspector {
background: #444444;
}
@@ -774,4 +768,4 @@ QProgressBar::chunk {
#gemRepoInspectorAddInfoTitleLabel {
font-size: 16px;
color: #FFFFFF;
}
}
@@ -0,0 +1,118 @@
/*
* 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 <AdjustableHeaderWidget.h>
#include <AzCore/Debug/Trace.h>
#include <QHeaderView>
#include <QTimer>
namespace O3DE::ProjectManager
{
AdjustableHeaderWidget::AdjustableHeaderWidget( const QStringList& headerLabels,
const QVector<int>& defaultHeaderWidths, int minHeaderWidth,
const QVector<QHeaderView::ResizeMode>& resizeModes, QWidget* parent)
: QTableWidget(parent)
{
setObjectName("adjustableHeaderWidget");
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
setFixedHeight(s_headerWidgetHeight);
m_header = horizontalHeader();
m_header->setDefaultAlignment(Qt::AlignLeft);
setColumnCount(headerLabels.count());
setHorizontalHeaderLabels(headerLabels);
AZ_Assert(defaultHeaderWidths.count() == columnCount(), "Default header widths does not match number of columns");
AZ_Assert(resizeModes.count() == columnCount(), "Resize modesdoes not match number of columns");
for (int column = 0; column < columnCount(); ++column)
{
m_header->resizeSection(column, defaultHeaderWidths[column]);
m_header->setSectionResizeMode(column, resizeModes[column]);
}
m_header->setMinimumSectionSize(minHeaderWidth);
m_header->setCascadingSectionResizes(true);
connect(m_header, &QHeaderView::sectionResized, this, &AdjustableHeaderWidget::OnSectionResized);
}
void AdjustableHeaderWidget::OnSectionResized(int logicalIndex, int oldSize, int newSize)
{
const int headerCount = columnCount();
const int headerWidth = m_header->width();
const int totalSectionWidth = m_header->length();
if (totalSectionWidth > headerWidth && newSize > oldSize)
{
int xPos = 0;
int requiredWidth = 0;
for (int i = 0; i < headerCount; i++)
{
if (i < logicalIndex)
{
xPos += m_header->sectionSize(i);
}
else if (i == logicalIndex)
{
xPos += newSize;
}
else if (i > logicalIndex)
{
if (m_header->sectionResizeMode(i) == QHeaderView::ResizeMode::Fixed)
{
requiredWidth += m_header->sectionSize(i);
}
else
{
requiredWidth += m_header->minimumSectionSize();
}
}
}
if (xPos + requiredWidth > headerWidth)
{
m_header->resizeSection(logicalIndex, oldSize);
}
}
// wait till all columns resized
QTimer::singleShot(0, [&]()
{
// only re-paint when the header and section widths have settled
const int headerWidth = m_header->width();
const int totalSectionWidth = m_header->length();
if (totalSectionWidth == headerWidth)
{
emit sectionsResized();
}
});
}
QPair<int, int> AdjustableHeaderWidget::CalcColumnXBounds(int headerIndex) const
{
// Total the widths of all headers before this one in first and including it in second
QPair<int, int> bounds(0, 0);
for (int curIndex = 0; curIndex <= headerIndex; ++curIndex)
{
if (curIndex == headerIndex)
{
bounds.first = bounds.second;
}
bounds.second += m_header->sectionSize(curIndex);
}
return bounds;
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,52 @@
/*
* 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 <QTableWidget>
#include <QHeaderView>
#include <QStringList>
#include <QVector>
#include <QPair>
#endif
namespace O3DE::ProjectManager
{
// Using a QTableWidget for its header
// Using a seperate model allows the setup of a header exactly as needed
class AdjustableHeaderWidget
: public QTableWidget
{
Q_OBJECT
public:
explicit AdjustableHeaderWidget(const QStringList& headerLabels,
const QVector<int>& defaultHeaderWidths, int minHeaderWidth,
const QVector<QHeaderView::ResizeMode>& resizeModes,
QWidget* parent = nullptr);
~AdjustableHeaderWidget() = default;
QPair<int, int> CalcColumnXBounds(int headerIndex) const;
inline constexpr static int s_headerTextIndent = 7;
inline constexpr static int s_headerWidgetHeight = 24;
QHeaderView* m_header;
signals:
void sectionsResized();
protected slots:
void OnSectionResized(int logicalIndex, int oldSize, int newSize);
private:
inline constexpr static int s_headerIndentSection = 11;
};
} // namespace O3DE::ProjectManager
@@ -17,7 +17,7 @@ namespace O3DE::ProjectManager
class ExternalLinkDialog
: public QDialog
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit ExternalLinkDialog(const QUrl& url, QWidget* parent = nullptr);
~ExternalLinkDialog() = default;
@@ -33,7 +33,7 @@ namespace O3DE::ProjectManager
class GemCartWidget
: public QScrollArea
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
GemCartWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
@@ -19,8 +19,10 @@
#include <GemCatalog/GemDependenciesDialog.h>
#include <GemCatalog/GemUpdateDialog.h>
#include <GemCatalog/GemUninstallDialog.h>
#include <GemCatalog/GemItemDelegate.h>
#include <DownloadController.h>
#include <ProjectUtils.h>
#include <AdjustableHeaderWidget.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -40,6 +42,13 @@ namespace O3DE::ProjectManager
GemCatalogScreen::GemCatalogScreen(QWidget* parent)
: ScreenWidget(parent)
{
// The width of either side panel (filters, inspector) in the catalog
constexpr int sidePanelWidth = 240;
// Querying qApp about styling reports the scroll bar being larger than it is so define it manually
constexpr int verticalScrollBarWidth = 8;
setObjectName("GemCatalogScreen");
m_gemModel = new GemModel(this);
m_proxyModel = new GemSortFilterProxyModel(m_gemModel, this);
@@ -69,10 +78,8 @@ namespace O3DE::ProjectManager
hLayout->setMargin(0);
vLayout->addLayout(hLayout);
m_gemListView = new GemListView(m_proxyModel, m_proxyModel->GetSelectionModel(), this);
m_rightPanelStack = new QStackedWidget(this);
m_rightPanelStack->setFixedWidth(240);
m_rightPanelStack->setFixedWidth(sidePanelWidth);
m_gemInspector = new GemInspector(m_gemModel, this);
@@ -81,18 +88,45 @@ namespace O3DE::ProjectManager
connect(m_gemInspector, &GemInspector::UninstallGem, this, &GemCatalogScreen::UninstallGem);
QWidget* filterWidget = new QWidget(this);
filterWidget->setFixedWidth(240);
filterWidget->setFixedWidth(sidePanelWidth);
m_filterWidgetLayout = new QVBoxLayout();
m_filterWidgetLayout->setMargin(0);
m_filterWidgetLayout->setSpacing(0);
filterWidget->setLayout(m_filterWidgetLayout);
GemListHeaderWidget* listHeaderWidget = new GemListHeaderWidget(m_proxyModel);
GemListHeaderWidget* catalogHeaderWidget = new GemListHeaderWidget(m_proxyModel);
constexpr int minHeaderSectionWidth = 100;
AdjustableHeaderWidget* listHeaderWidget = new AdjustableHeaderWidget(
QStringList{ tr("Gem Name"), tr("Gem Summary"), tr("Status") },
QVector<int>{
GemItemDelegate::s_defaultSummaryStartX - 30,
0, // Section is set to stretch to fit
GemItemDelegate::s_buttonWidth + GemItemDelegate::s_itemMargins.left() + GemItemDelegate::s_itemMargins.right() + GemItemDelegate::s_contentMargins.right()
},
minHeaderSectionWidth,
QVector<QHeaderView::ResizeMode>
{
QHeaderView::ResizeMode::Interactive,
QHeaderView::ResizeMode::Stretch,
QHeaderView::ResizeMode::Fixed
},
this);
m_gemListView = new GemListView(m_proxyModel, m_proxyModel->GetSelectionModel(), listHeaderWidget, this);
QHBoxLayout* listHeaderLayout = new QHBoxLayout();
listHeaderLayout->setMargin(0);
listHeaderLayout->setSpacing(0);
listHeaderLayout->addSpacing(GemItemDelegate::s_itemMargins.left());
listHeaderLayout->addWidget(listHeaderWidget);
listHeaderLayout->addSpacing(GemItemDelegate::s_itemMargins.right() + verticalScrollBarWidth);
QVBoxLayout* middleVLayout = new QVBoxLayout();
middleVLayout->setMargin(0);
middleVLayout->setSpacing(0);
middleVLayout->addWidget(listHeaderWidget);
middleVLayout->addWidget(catalogHeaderWidget);
middleVLayout->addLayout(listHeaderLayout);
middleVLayout->addWidget(m_gemListView);
hLayout->addWidget(filterWidget);
@@ -19,7 +19,7 @@ namespace O3DE::ProjectManager
class GemDependenciesDialog
: public QDialog
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemDependenciesDialog(GemModel* gemModel, QWidget *parent = nullptr);
~GemDependenciesDialog() = default;
@@ -26,7 +26,7 @@ namespace O3DE::ProjectManager
class FilterCategoryWidget
: public QWidget
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit FilterCategoryWidget(const QString& header,
@@ -28,7 +28,7 @@ namespace O3DE::ProjectManager
class GemInspector
: public QScrollArea
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemInspector(GemModel* model, QWidget* parent = nullptr);
@@ -9,6 +9,8 @@
#include <GemCatalog/GemItemDelegate.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <AdjustableHeaderWidget.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <QEvent>
@@ -22,12 +24,14 @@
#include <QAbstractTextDocumentLayout>
#include <QDesktopServices>
#include <QMovie>
#include <QHeaderView>
namespace O3DE::ProjectManager
{
GemItemDelegate::GemItemDelegate(QAbstractItemModel* model, QObject* parent)
GemItemDelegate::GemItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent)
: QStyledItemDelegate(parent)
, m_model(model)
, m_headerWidget(header)
{
AddPlatformIcon(GemInfo::Android, ":/Android.svg");
AddPlatformIcon(GemInfo::iOS, ":/iOS.svg");
@@ -116,12 +120,15 @@ namespace O3DE::ProjectManager
// Gem name
QString gemName = GemModel::GetDisplayName(modelIndex);
QFont gemNameFont(options.font);
const int firstColumnMaxTextWidth = s_summaryStartX - 30;
QPair<int, int> nameXBounds = CalcColumnXBounds(HeaderOrder::Name);
const int nameStartX = nameXBounds.first;
const int firstColumnTextStartX = s_itemMargins.left() + nameStartX + AdjustableHeaderWidget::s_headerTextIndent;
const int firstColumnMaxTextWidth = nameXBounds.second - nameStartX - AdjustableHeaderWidget::s_headerTextIndent;
gemNameFont.setPixelSize(static_cast<int>(s_gemNameFontSize));
gemNameFont.setBold(true);
gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
QRect gemNameRect = GetTextRect(gemNameFont, gemName, s_gemNameFontSize);
gemNameRect.moveTo(contentRect.left(), contentRect.top());
gemNameRect.moveTo(firstColumnTextStartX, contentRect.top());
painter->setFont(gemNameFont);
painter->setPen(m_textColor);
gemNameRect = painter->boundingRect(gemNameRect, Qt::TextSingleLine, gemName);
@@ -131,7 +138,7 @@ namespace O3DE::ProjectManager
QString gemCreator = GemModel::GetCreator(modelIndex);
gemCreator = standardFontMetrics.elidedText(gemCreator, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
QRect gemCreatorRect = GetTextRect(standardFont, gemCreator, s_fontSize);
gemCreatorRect.moveTo(contentRect.left(), contentRect.top() + gemNameRect.height());
gemCreatorRect.moveTo(firstColumnTextStartX, contentRect.top() + gemNameRect.height());
painter->setFont(standardFont);
gemCreatorRect = painter->boundingRect(gemCreatorRect, Qt::TextSingleLine, gemCreator);
@@ -157,10 +164,13 @@ namespace O3DE::ProjectManager
const int featureTagAreaHeight = 30;
const int summaryHeight = contentRect.height() - (hasTags * featureTagAreaHeight);
const int additionalSummarySpacing = s_itemMargins.right() * 3;
const QSize summarySize = QSize(contentRect.width() - s_summaryStartX - s_buttonWidth - additionalSummarySpacing,
const auto [summaryStartX, summaryEndX] = CalcColumnXBounds(HeaderOrder::Summary);
const QSize summarySize =
QSize(summaryEndX - summaryStartX - AdjustableHeaderWidget::s_headerTextIndent - s_extraSummarySpacing,
summaryHeight);
return QRect(QPoint(contentRect.left() + s_summaryStartX, contentRect.top()), summarySize);
return QRect(
QPoint(s_itemMargins.left() + summaryStartX + AdjustableHeaderWidget::s_headerTextIndent, contentRect.top()), summarySize);
}
QSize GemItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const
@@ -169,7 +179,7 @@ namespace O3DE::ProjectManager
initStyleOption(&options, modelIndex);
int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right();
return QSize(marginsHorizontal + s_buttonWidth + s_summaryStartX, s_height);
return QSize(marginsHorizontal + s_buttonWidth + s_defaultSummaryStartX, s_height);
}
bool GemItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex)
@@ -299,9 +309,17 @@ namespace O3DE::ProjectManager
return QFontMetrics(font).boundingRect(text);
}
QPair<int, int> GemItemDelegate::CalcColumnXBounds(HeaderOrder header) const
{
return m_headerWidget->CalcColumnXBounds(static_cast<int>(header));
}
QRect GemItemDelegate::CalcButtonRect(const QRect& contentRect) const
{
const QPoint topLeft = QPoint(contentRect.right() - s_buttonWidth, contentRect.center().y() - s_buttonHeight / 2);
const QPoint topLeft = QPoint(
s_itemMargins.left() + CalcColumnXBounds(HeaderOrder::Status).first + AdjustableHeaderWidget::s_headerTextIndent + s_statusIconSize +
s_statusButtonSpacing,
contentRect.center().y() - s_buttonHeight / 2);
const QSize size = QSize(s_buttonWidth, s_buttonHeight);
return QRect(topLeft, size);
}
@@ -331,18 +349,23 @@ namespace O3DE::ProjectManager
}
}
void GemItemDelegate::DrawFeatureTags(QPainter* painter, const QRect& contentRect, const QStringList& featureTags, const QFont& standardFont, const QRect& summaryRect) const
void GemItemDelegate::DrawFeatureTags(
QPainter* painter,
const QRect& contentRect,
const QStringList& featureTags,
const QFont& standardFont,
const QRect& summaryRect) const
{
QFont gemFeatureTagFont(standardFont);
gemFeatureTagFont.setPixelSize(s_featureTagFontSize);
gemFeatureTagFont.setBold(false);
painter->setFont(gemFeatureTagFont);
int x = s_summaryStartX;
int x = CalcColumnXBounds(HeaderOrder::Summary).first + AdjustableHeaderWidget::s_headerTextIndent;
for (const QString& featureTag : featureTags)
{
QRect featureTagRect = GetTextRect(gemFeatureTagFont, featureTag, s_featureTagFontSize);
featureTagRect.moveTo(contentRect.left() + x + s_featureTagBorderMarginX,
featureTagRect.moveTo(s_itemMargins.left() + x + s_featureTagBorderMarginX,
contentRect.top() + 47);
featureTagRect = painter->boundingRect(featureTagRect, Qt::TextSingleLine, featureTag);
@@ -19,13 +19,15 @@ QT_FORWARD_DECLARE_CLASS(QEvent)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget)
class GemItemDelegate
: public QStyledItemDelegate
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemItemDelegate(QAbstractItemModel* model, QObject* parent = nullptr);
explicit GemItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent = nullptr);
~GemItemDelegate() = default;
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override;
@@ -45,12 +47,13 @@ namespace O3DE::ProjectManager
inline constexpr static int s_height = 105; // Gem item total height
inline constexpr static qreal s_gemNameFontSize = 13.0;
inline constexpr static qreal s_fontSize = 12.0;
inline constexpr static int s_summaryStartX = 150;
inline constexpr static int s_defaultSummaryStartX = 190;
// Margin and borders
inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/16, /*top=*/8, /*right=*/16, /*bottom=*/8); // Item border distances
inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/20, /*top=*/12, /*right=*/20, /*bottom=*/12); // Distances of the elements within an item to the item borders
inline constexpr static int s_borderWidth = 4;
inline constexpr static int s_extraSummarySpacing = s_itemMargins.right();
// Button
inline constexpr static int s_buttonWidth = 32;
@@ -65,6 +68,13 @@ namespace O3DE::ProjectManager
inline constexpr static int s_featureTagBorderMarginY = 3;
inline constexpr static int s_featureTagSpacing = 7;
enum class HeaderOrder
{
Name,
Summary,
Status
};
signals:
void MovieStartedPlaying(const QMovie* playingMovie) const;
@@ -74,13 +84,20 @@ namespace O3DE::ProjectManager
void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const;
QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const;
QPair<int, int> CalcColumnXBounds(HeaderOrder header) const;
QRect CalcButtonRect(const QRect& contentRect) const;
QRect CalcSummaryRect(const QRect& contentRect, bool hasTags) const;
void DrawPlatformIcons(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const;
void DrawButton(QPainter* painter, const QRect& buttonRect, const QModelIndex& modelIndex) const;
void DrawFeatureTags(QPainter* painter, const QRect& contentRect, const QStringList& featureTags, const QFont& standardFont, const QRect& summaryRect) const;
void DrawFeatureTags(
QPainter* painter,
const QRect& contentRect,
const QStringList& featureTags,
const QFont& standardFont,
const QRect& summaryRect) const;
void DrawText(const QString& text, QPainter* painter, const QRect& rect, const QFont& standardFont) const;
void DrawDownloadStatusIcon(QPainter* painter, const QRect& contentRect, const QRect& buttonRect, const QModelIndex& modelIndex) const;
void DrawDownloadStatusIcon(
QPainter* painter, const QRect& contentRect, const QRect& buttonRect, const QModelIndex& modelIndex) const;
QAbstractItemModel* m_model = nullptr;
@@ -100,5 +117,7 @@ namespace O3DE::ProjectManager
QPixmap m_downloadSuccessfulPixmap;
QPixmap m_downloadFailedPixmap;
QMovie* m_downloadingMovie = nullptr;
AdjustableHeaderWidget* m_headerWidget = nullptr;
};
} // namespace O3DE::ProjectManager
@@ -78,37 +78,9 @@ namespace O3DE::ProjectManager
// Separating line
QFrame* hLine = new QFrame();
hLine->setFrameShape(QFrame::HLine);
hLine->setStyleSheet("color: #666666;");
hLine->setObjectName("horizontalSeparatingLine");
vLayout->addWidget(hLine);
vLayout->addSpacing(GemItemDelegate::s_contentMargins.top());
// Bottom section
QHBoxLayout* columnHeaderLayout = new QHBoxLayout();
columnHeaderLayout->setAlignment(Qt::AlignLeft);
const int gemNameStartX = GemItemDelegate::s_itemMargins.left() + GemItemDelegate::s_contentMargins.left() - 1;
columnHeaderLayout->addSpacing(gemNameStartX);
QLabel* gemNameLabel = new QLabel(tr("Gem Name"));
gemNameLabel->setObjectName("GemCatalogHeaderLabel");
columnHeaderLayout->addWidget(gemNameLabel);
columnHeaderLayout->addSpacing(89);
QLabel* gemSummaryLabel = new QLabel(tr("Gem Summary"));
gemSummaryLabel->setObjectName("GemCatalogHeaderLabel");
columnHeaderLayout->addWidget(gemSummaryLabel);
QSpacerItem* horizontalSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
columnHeaderLayout->addSpacerItem(horizontalSpacer);
QLabel* gemSelectedLabel = new QLabel(tr("Status"));
gemSelectedLabel->setObjectName("GemCatalogHeaderLabel");
columnHeaderLayout->addWidget(gemSelectedLabel);
columnHeaderLayout->addSpacing(72);
vLayout->addLayout(columnHeaderLayout);
}
} // namespace O3DE::ProjectManager
@@ -19,7 +19,7 @@ namespace O3DE::ProjectManager
class GemListHeaderWidget
: public QFrame
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemListHeaderWidget(GemSortFilterProxyModel* proxyModel, QWidget* parent = nullptr);
@@ -8,12 +8,15 @@
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemItemDelegate.h>
#include <AdjustableHeaderWidget.h>
#include <QMovie>
#include <QHeaderView>
namespace O3DE::ProjectManager
{
GemListView::GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent)
GemListView::GemListView(
QAbstractItemModel* model, QItemSelectionModel* selectionModel, AdjustableHeaderWidget* header, QWidget* parent)
: QListView(parent)
{
setObjectName("GemCatalogListView");
@@ -21,7 +24,7 @@ namespace O3DE::ProjectManager
setModel(model);
setSelectionModel(selectionModel);
GemItemDelegate* itemDelegate = new GemItemDelegate(model, this);
GemItemDelegate* itemDelegate = new GemItemDelegate(model, header, this);
connect(itemDelegate, &GemItemDelegate::MovieStartedPlaying, [=](const QMovie* playingMovie)
{
@@ -31,6 +34,8 @@ namespace O3DE::ProjectManager
this->viewport()->repaint();
});
});
connect(header, &AdjustableHeaderWidget::sectionsResized, [=] { update(); });
setItemDelegate(itemDelegate);
}
@@ -16,13 +16,15 @@
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget)
class GemListView
: public QListView
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr);
explicit GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, AdjustableHeaderWidget* header, QWidget* parent = nullptr);
~GemListView() = default;
};
} // namespace O3DE::ProjectManager
@@ -21,7 +21,7 @@ namespace O3DE::ProjectManager
class GemModel
: public QStandardItemModel
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemModel(QObject* parent = nullptr);
@@ -16,7 +16,7 @@
namespace O3DE::ProjectManager
{
GemRequirementDelegate::GemRequirementDelegate(QAbstractItemModel* model, QObject* parent)
: GemItemDelegate(model, parent)
: GemItemDelegate(model, nullptr, parent)
{
}
@@ -54,7 +54,7 @@ namespace O3DE::ProjectManager
// Gem name
QString gemName = GemModel::GetDisplayName(modelIndex);
QFont gemNameFont(options.font);
const int firstColumnMaxTextWidth = s_summaryStartX - 30;
const int firstColumnMaxTextWidth = s_defaultSummaryStartX - 30;
gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
gemNameFont.setPixelSize(static_cast<int>(s_gemNameFontSize));
gemNameFont.setBold(true);
@@ -75,8 +75,8 @@ namespace O3DE::ProjectManager
QRect GemRequirementDelegate::CalcRequirementRect(const QRect& contentRect) const
{
const QSize requirementSize = QSize(contentRect.width() - s_summaryStartX - s_itemMargins.right(), contentRect.height());
return QRect(QPoint(contentRect.left() + s_summaryStartX, contentRect.top()), requirementSize);
const QSize requirementSize = QSize(contentRect.width() - s_defaultSummaryStartX - s_itemMargins.right(), contentRect.height());
return QRect(QPoint(contentRect.left() + s_defaultSummaryStartX, contentRect.top()), requirementSize);
}
bool GemRequirementDelegate::editorEvent(
@@ -18,7 +18,7 @@ namespace O3DE::ProjectManager
class GemRequirementDelegate
: public GemItemDelegate
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemRequirementDelegate(QAbstractItemModel* model, QObject* parent = nullptr);
@@ -19,7 +19,7 @@ namespace O3DE::ProjectManager
class GemRequirementDialog
: public QDialog
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemRequirementDialog(GemModel* model, QWidget *parent = nullptr);
~GemRequirementDialog() = default;
@@ -22,7 +22,7 @@ namespace O3DE::ProjectManager
class GemRequirementFilterProxyModel
: public QSortFilterProxyModel
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
GemRequirementFilterProxyModel(GemModel* sourceModel, QObject* parent = nullptr);
@@ -19,7 +19,7 @@ namespace O3DE::ProjectManager
class GemRequirementListView
: public QListView
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemRequirementListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr);
@@ -22,7 +22,7 @@ namespace O3DE::ProjectManager
class GemSortFilterProxyModel
: public QSortFilterProxyModel
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
enum class GemSelected
@@ -17,7 +17,7 @@ namespace O3DE::ProjectManager
class GemUninstallDialog
: public QDialog
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemUninstallDialog(const QString& gemName, QWidget *parent = nullptr);
~GemUninstallDialog() = default;
@@ -17,7 +17,7 @@ namespace O3DE::ProjectManager
class GemUpdateDialog
: public QDialog
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public :
explicit GemUpdateDialog(const QString& gemName, bool updateAvaliable = true, QWidget* parent = nullptr);
~GemUpdateDialog() = default;
@@ -26,7 +26,7 @@ namespace O3DE::ProjectManager
{
class GemRepoInspector : public QScrollArea
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public : explicit GemRepoInspector(GemRepoModel* model, QWidget* parent = nullptr);
~GemRepoInspector() = default;
@@ -9,16 +9,19 @@
#include <GemRepo/GemRepoItemDelegate.h>
#include <GemRepo/GemRepoModel.h>
#include <ProjectManagerDefs.h>
#include <AdjustableHeaderWidget.h>
#include <QEvent>
#include <QPainter>
#include <QMouseEvent>
#include <QHeaderView>
namespace O3DE::ProjectManager
{
GemRepoItemDelegate::GemRepoItemDelegate(QAbstractItemModel* model, QObject* parent)
GemRepoItemDelegate::GemRepoItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent)
: QStyledItemDelegate(parent)
, m_model(model)
, m_headerWidget(header)
{
m_refreshIcon = QIcon(":/Refresh.svg").pixmap(s_refreshIconSize, s_refreshIconSize);
m_editIcon = QIcon(":/Edit.svg").pixmap(s_iconSize, s_iconSize);
@@ -69,44 +72,55 @@ namespace O3DE::ProjectManager
painter->restore();
}
int currentHorizontalOffset = CalcColumnXBounds(HeaderOrder::Name).first;
// Repo name
QString repoName = GemRepoModel::GetName(modelIndex);
repoName = QFontMetrics(standardFont).elidedText(repoName, Qt::TextElideMode::ElideRight, s_nameMaxWidth);
int sectionSize = m_headerWidget->m_header->sectionSize(static_cast<int>(HeaderOrder::Name));
repoName = standardFontMetrics.elidedText(repoName, Qt::TextElideMode::ElideRight,
sectionSize - AdjustableHeaderWidget::s_headerTextIndent);
QRect repoNameRect = GetTextRect(standardFont, repoName, s_fontSize);
int currentHorizontalOffset = contentRect.left();
repoNameRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoNameRect.height() / 2);
repoNameRect.moveTo(currentHorizontalOffset + AdjustableHeaderWidget::s_headerTextIndent,
contentRect.center().y() - repoNameRect.height() / 2);
repoNameRect = painter->boundingRect(repoNameRect, Qt::TextSingleLine, repoName);
painter->drawText(repoNameRect, Qt::TextSingleLine, repoName);
// Rem repo creator
currentHorizontalOffset += sectionSize;
sectionSize = m_headerWidget->m_header->sectionSize(static_cast<int>(HeaderOrder::Creator));
QString repoCreator = GemRepoModel::GetCreator(modelIndex);
repoCreator = standardFontMetrics.elidedText(repoCreator, Qt::TextElideMode::ElideRight, s_creatorMaxWidth);
repoCreator = standardFontMetrics.elidedText(repoCreator, Qt::TextElideMode::ElideRight,
sectionSize - AdjustableHeaderWidget::s_headerTextIndent);
QRect repoCreatorRect = GetTextRect(standardFont, repoCreator, s_fontSize);
currentHorizontalOffset += s_nameMaxWidth + s_contentSpacing;
repoCreatorRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoCreatorRect.height() / 2);
repoCreatorRect.moveTo(currentHorizontalOffset + AdjustableHeaderWidget::s_headerTextIndent,
contentRect.center().y() - repoCreatorRect.height() / 2);
repoCreatorRect = painter->boundingRect(repoCreatorRect, Qt::TextSingleLine, repoCreator);
painter->drawText(repoCreatorRect, Qt::TextSingleLine, repoCreator);
// Repo update
currentHorizontalOffset += sectionSize;
sectionSize = m_headerWidget->m_header->sectionSize(static_cast<int>(HeaderOrder::Update));
QString repoUpdatedDate = GemRepoModel::GetLastUpdated(modelIndex).toString(RepoTimeFormat);
repoUpdatedDate = standardFontMetrics.elidedText(repoUpdatedDate, Qt::TextElideMode::ElideRight, s_updatedMaxWidth);
repoUpdatedDate = standardFontMetrics.elidedText(
repoUpdatedDate, Qt::TextElideMode::ElideRight,
sectionSize - GemRepoItemDelegate::s_refreshIconSpacing - GemRepoItemDelegate::s_refreshIconSize - AdjustableHeaderWidget::s_headerTextIndent);
QRect repoUpdatedDateRect = GetTextRect(standardFont, repoUpdatedDate, s_fontSize);
currentHorizontalOffset += s_creatorMaxWidth + s_contentSpacing;
repoUpdatedDateRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoUpdatedDateRect.height() / 2);
repoUpdatedDateRect.moveTo(currentHorizontalOffset + AdjustableHeaderWidget::s_headerTextIndent,
contentRect.center().y() - repoUpdatedDateRect.height() / 2);
repoUpdatedDateRect = painter->boundingRect(repoUpdatedDateRect, Qt::TextSingleLine, repoUpdatedDate);
painter->drawText(repoUpdatedDateRect, Qt::TextSingleLine, repoUpdatedDate);
// Draw refresh button
painter->drawPixmap(
repoUpdatedDateRect.left() + s_updatedMaxWidth + s_refreshIconSpacing,
contentRect.center().y() - s_refreshIconSize / 3, // Dividing size by 3 centers much better
m_refreshIcon);
const QRect refreshButtonRect = CalcRefreshButtonRect(contentRect);
painter->drawPixmap(refreshButtonRect.topLeft(), m_refreshIcon);
if (options.state & QStyle::State_MouseOver)
{
@@ -121,8 +135,8 @@ namespace O3DE::ProjectManager
QStyleOptionViewItem options(option);
initStyleOption(&options, modelIndex);
int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right();
return QSize(marginsHorizontal + s_nameMaxWidth + s_creatorMaxWidth + s_updatedMaxWidth + s_contentSpacing * 3, s_height);
const int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right();
return QSize(marginsHorizontal + s_nameDefaultWidth + s_creatorDefaultWidth + s_updatedDefaultWidth, s_height);
}
bool GemRepoItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex)
@@ -185,22 +199,31 @@ namespace O3DE::ProjectManager
return QFontMetrics(font).boundingRect(text);
}
QPair<int, int> GemRepoItemDelegate::CalcColumnXBounds(HeaderOrder header) const
{
return m_headerWidget->CalcColumnXBounds(static_cast<int>(header));
}
QRect GemRepoItemDelegate::CalcDeleteButtonRect(const QRect& contentRect) const
{
const QPoint topLeft = QPoint(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2);
const int deleteHeaderEndX = CalcColumnXBounds(HeaderOrder::Delete).second;
const QPoint topLeft = QPoint(deleteHeaderEndX - s_iconSize - s_contentMargins.right(), contentRect.center().y() - s_iconSize / 2);
return QRect(topLeft, QSize(s_iconSize, s_iconSize));
}
QRect GemRepoItemDelegate::CalcRefreshButtonRect(const QRect& contentRect) const
{
const int topLeftX = contentRect.left() + s_nameMaxWidth + s_creatorMaxWidth + s_updatedMaxWidth + s_contentSpacing * 2 + s_refreshIconSpacing;
const QPoint topLeft = QPoint(topLeftX, contentRect.center().y() - s_refreshIconSize / 3);
const int headerEndX = CalcColumnXBounds(HeaderOrder::Update).second;
const int leftX = headerEndX - s_refreshIconSize - s_refreshIconSpacing;
// Dividing size by 3 centers much better
const QPoint topLeft = QPoint(leftX, contentRect.center().y() - s_refreshIconSize / 3);
return QRect(topLeft, QSize(s_refreshIconSize, s_refreshIconSize));
}
void GemRepoItemDelegate::DrawEditButtons(QPainter* painter, const QRect& contentRect) const
{
painter->drawPixmap(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2, m_deleteIcon);
const QRect deleteButtonRect = CalcDeleteButtonRect(contentRect);
painter->drawPixmap(deleteButtonRect, m_deleteIcon);
}
} // namespace O3DE::ProjectManager
@@ -18,13 +18,15 @@ QT_FORWARD_DECLARE_CLASS(QEvent)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget)
class GemRepoItemDelegate
: public QStyledItemDelegate
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemRepoItemDelegate(QAbstractItemModel* model, QObject* parent = nullptr);
explicit GemRepoItemDelegate(QAbstractItemModel* model, AdjustableHeaderWidget* header, QObject* parent = nullptr);
~GemRepoItemDelegate() = default;
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override;
@@ -42,15 +44,14 @@ namespace O3DE::ProjectManager
inline constexpr static qreal s_fontSize = 12.0;
// Margin and borders
inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/0, /*top=*/8, /*right=*/60, /*bottom=*/8); // Item border distances
inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/0, /*top=*/8, /*right=*/0, /*bottom=*/8); // Item border distances
inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/20, /*top=*/20, /*right=*/20, /*bottom=*/20); // Distances of the elements within an item to the item borders
inline constexpr static int s_borderWidth = 4;
// Content
inline constexpr static int s_contentSpacing = 5;
inline constexpr static int s_nameMaxWidth = 145;
inline constexpr static int s_creatorMaxWidth = 115;
inline constexpr static int s_updatedMaxWidth = 125;
inline constexpr static int s_nameDefaultWidth = 150;
inline constexpr static int s_creatorDefaultWidth = 120;
inline constexpr static int s_updatedDefaultWidth = 130;
// Icon
inline constexpr static int s_iconSize = 24;
@@ -58,6 +59,14 @@ namespace O3DE::ProjectManager
inline constexpr static int s_refreshIconSize = 14;
inline constexpr static int s_refreshIconSpacing = 10;
enum class HeaderOrder
{
Name,
Creator,
Update,
Delete
};
signals:
void RemoveRepo(const QModelIndex& modelIndex);
void RefreshRepo(const QModelIndex& modelIndex);
@@ -65,13 +74,15 @@ namespace O3DE::ProjectManager
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;
QPair<int, int> CalcColumnXBounds(HeaderOrder header) const;
QRect CalcDeleteButtonRect(const QRect& contentRect) const;
QRect CalcRefreshButtonRect(const QRect& contentRect) const;
void DrawEditButtons(QPainter* painter, const QRect& contentRect) const;
QAbstractItemModel* m_model = nullptr;
AdjustableHeaderWidget* m_headerWidget = nullptr;
QPixmap m_refreshIcon;
QPixmap m_editIcon;
QPixmap m_deleteIcon;
@@ -8,12 +8,15 @@
#include <GemRepo/GemRepoListView.h>
#include <GemRepo/GemRepoItemDelegate.h>
#include <AdjustableHeaderWidget.h>
#include <QShortcut>
#include <QHeaderView>
namespace O3DE::ProjectManager
{
GemRepoListView::GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent)
GemRepoListView::GemRepoListView(
QAbstractItemModel* model, QItemSelectionModel* selectionModel, AdjustableHeaderWidget* header, QWidget* parent)
: QListView(parent)
{
setObjectName("gemRepoListView");
@@ -22,9 +25,10 @@ namespace O3DE::ProjectManager
setModel(model);
setSelectionModel(selectionModel);
GemRepoItemDelegate* itemDelegate = new GemRepoItemDelegate(model, this);
GemRepoItemDelegate* itemDelegate = new GemRepoItemDelegate(model, header, this);
connect(itemDelegate, &GemRepoItemDelegate::RemoveRepo, this, &GemRepoListView::RemoveRepo);
connect(itemDelegate, &GemRepoItemDelegate::RefreshRepo, this, &GemRepoListView::RefreshRepo);
connect(header, &AdjustableHeaderWidget::sectionsResized, [=] { update(); });
setItemDelegate(itemDelegate);
}
} // namespace O3DE::ProjectManager
@@ -17,13 +17,19 @@ QT_FORWARD_DECLARE_CLASS(QAbstractItemModel)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget)
class GemRepoListView
: public QListView
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemRepoListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr);
explicit GemRepoListView(
QAbstractItemModel* model,
QItemSelectionModel* selectionModel,
AdjustableHeaderWidget* header,
QWidget* parent = nullptr);
~GemRepoListView() = default;
signals:
@@ -21,7 +21,7 @@ namespace O3DE::ProjectManager
class GemRepoModel
: public QStandardItemModel
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
explicit GemRepoModel(QObject* parent = nullptr);
@@ -15,6 +15,8 @@
#include <PythonBindingsInterface.h>
#include <ProjectManagerDefs.h>
#include <ProjectUtils.h>
#include <AdjustableHeaderWidget.h>
#include <ProjectManagerDefs.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -248,6 +250,9 @@ namespace O3DE::ProjectManager
QFrame* GemRepoScreen::CreateReposContent()
{
constexpr int inspectorWidth = 240;
constexpr int middleLayoutIndent = 60;
QFrame* contentFrame = new QFrame(this);
QHBoxLayout* hLayout = new QHBoxLayout();
@@ -255,7 +260,7 @@ namespace O3DE::ProjectManager
hLayout->setSpacing(0);
contentFrame->setLayout(hLayout);
hLayout->addSpacing(60);
hLayout->addSpacing(middleLayoutIndent);
QVBoxLayout* middleVLayout = new QVBoxLayout();
middleVLayout->setMargin(0);
@@ -287,37 +292,34 @@ namespace O3DE::ProjectManager
connect(addRepoButton, &QPushButton::clicked, this, &GemRepoScreen::HandleAddRepoButton);
topMiddleHLayout->addSpacing(30);
middleVLayout->addLayout(topMiddleHLayout);
middleVLayout->addSpacing(30);
// Create a QTableWidget just for its header
// Using a seperate model allows the setup of a header exactly as needed
m_gemRepoHeaderTable = new QTableWidget(this);
m_gemRepoHeaderTable->setObjectName("gemRepoHeaderTable");
m_gemRepoListHeader = m_gemRepoHeaderTable->horizontalHeader();
m_gemRepoListHeader->setObjectName("gemRepoListHeader");
m_gemRepoListHeader->setDefaultAlignment(Qt::AlignLeft);
m_gemRepoListHeader->setSectionResizeMode(QHeaderView::ResizeMode::Fixed);
constexpr int minHeaderSectionWidth = 120;
// Insert columns so the header labels will show up
m_gemRepoHeaderTable->insertColumn(0);
m_gemRepoHeaderTable->insertColumn(1);
m_gemRepoHeaderTable->insertColumn(2);
m_gemRepoHeaderTable->setHorizontalHeaderLabels({ tr("Repository Name"), tr("Creator"), tr("Updated") });
m_gemRepoHeaderTable = new AdjustableHeaderWidget(
QStringList{ tr("Repository Name"), tr("Creator"), tr("Updated"), "" },
QVector<int>{
GemRepoItemDelegate::s_nameDefaultWidth,
GemRepoItemDelegate::s_creatorDefaultWidth,
GemRepoItemDelegate::s_updatedDefaultWidth + GemRepoItemDelegate::s_refreshIconSpacing + GemRepoItemDelegate::s_refreshIconSize,
// Include invisible header for delete button
GemRepoItemDelegate::s_iconSize + GemRepoItemDelegate::s_contentMargins.right()
},
minHeaderSectionWidth,
QVector<QHeaderView::ResizeMode>
{
QHeaderView::ResizeMode::Interactive,
QHeaderView::ResizeMode::Stretch,
QHeaderView::ResizeMode::Fixed,
QHeaderView::ResizeMode::Fixed
},
this);
const int headerExtraMargin = 18;
m_gemRepoListHeader->resizeSection(0, GemRepoItemDelegate::s_nameMaxWidth + GemRepoItemDelegate::s_contentSpacing + headerExtraMargin);
m_gemRepoListHeader->resizeSection(1, GemRepoItemDelegate::s_creatorMaxWidth + GemRepoItemDelegate::s_contentSpacing);
m_gemRepoListHeader->resizeSection(2, GemRepoItemDelegate::s_updatedMaxWidth + GemRepoItemDelegate::s_contentSpacing);
// Required to set stylesheet in code as it will not be respected if set in qss
m_gemRepoHeaderTable->horizontalHeader()->setStyleSheet("QHeaderView::section { background-color:transparent; color:white; font-size:12px; border-style:none; }");
middleVLayout->addWidget(m_gemRepoHeaderTable);
m_gemRepoListView = new GemRepoListView(m_gemRepoModel, m_gemRepoModel->GetSelectionModel(), this);
m_gemRepoListView = new GemRepoListView(m_gemRepoModel, m_gemRepoModel->GetSelectionModel(), m_gemRepoHeaderTable, this);
middleVLayout->addWidget(m_gemRepoListView);
connect(m_gemRepoListView, &GemRepoListView::RemoveRepo, this, &GemRepoScreen::HandleRemoveRepoButton);
@@ -325,8 +327,10 @@ namespace O3DE::ProjectManager
hLayout->addLayout(middleVLayout);
hLayout->addSpacing(middleLayoutIndent);
m_gemRepoInspector = new GemRepoInspector(m_gemRepoModel, this);
m_gemRepoInspector->setFixedWidth(240);
m_gemRepoInspector->setFixedWidth(inspectorWidth);
hLayout->addWidget(m_gemRepoInspector);
return contentFrame;
@@ -24,6 +24,7 @@ namespace O3DE::ProjectManager
QT_FORWARD_DECLARE_CLASS(GemRepoInspector)
QT_FORWARD_DECLARE_CLASS(GemRepoListView)
QT_FORWARD_DECLARE_CLASS(GemRepoModel)
QT_FORWARD_DECLARE_CLASS(AdjustableHeaderWidget)
class GemRepoScreen
: public ScreenWidget
@@ -59,7 +60,7 @@ namespace O3DE::ProjectManager
QFrame* m_noRepoContent;
QFrame* m_repoContent;
QTableWidget* m_gemRepoHeaderTable = nullptr;
AdjustableHeaderWidget* m_gemRepoHeaderTable = nullptr;
QHeaderView* m_gemRepoListHeader = nullptr;
GemRepoListView* m_gemRepoListView = nullptr;
GemRepoInspector* m_gemRepoInspector = nullptr;
@@ -22,7 +22,7 @@ namespace O3DE::ProjectManager
class GemsSubWidget
: public QWidget
{
Q_OBJECT // AUTOMOC
Q_OBJECT
public:
GemsSubWidget(QWidget* parent = nullptr);

Some files were not shown because too many files have changed in this diff Show More