merging latest dev

Signed-off-by: Gene Walters <genewalt@amazon.com>
This commit is contained in:
Gene Walters
2021-10-22 17:00:47 -07:00
835 changed files with 23255 additions and 9255 deletions
+3 -1
View File
@@ -577,7 +577,9 @@ namespace AZ
}
azstrcat(lines[i], AZ_ARRAY_SIZE(lines[i]), "\n");
AZ_Printf(window, "%s", lines[i]); // feed back into the trace system so that listeners can get it.
// Use Output instead of AZ_Printf to be consistent with the exception output code and avoid
// this accidentally being suppressed as a normal message
Output(window, lines[i]);
}
}
}
@@ -0,0 +1,254 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
namespace AZ
{
JsonSerializationResult::ResultCode JsonImportResolver::ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element)
{
using namespace JsonSerializationResult;
for (auto& path : importPathStack)
{
if (importPath == path)
{
return settings.m_reporting(
AZStd::string::format("'%s' was already imported in this chain. This indicates a cyclic dependency.", importPath.c_str()),
ResultCode(Tasks::Import, Outcomes::Catastrophic), element);
}
}
importPathStack.push_back(importPath);
AZ::StackedString importElement(AZ::StackedString::Format::JsonPointer);
JsonImportSettings nestedImportSettings;
nestedImportSettings.m_importer = settings.m_importer;
nestedImportSettings.m_reporting = settings.m_reporting;
nestedImportSettings.m_resolveFlags = ImportTracking::Dependencies;
ResultCode result = ResolveImports(jsonDoc, allocator, importPathStack, nestedImportSettings, importElement);
importPathStack.pop_back();
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject())
{
for (auto& field : jsonDoc.GetObject())
{
if(strncmp(field.name.GetString(), JsonSerialization::ImportDirectiveIdentifier, field.name.GetStringLength()) == 0)
{
const rapidjson::Value& importDirective = field.value;
AZ::IO::FixedMaxPath importAbsPath = importPathStack.back();
importAbsPath.RemoveFilename();
AZStd::string importName;
if (importDirective.IsObject())
{
auto filenameField = importDirective.FindMember("filename");
if (filenameField != importDirective.MemberEnd())
{
importName = AZStd::string(filenameField->value.GetString(), filenameField->value.GetStringLength());
}
}
else
{
importName = AZStd::string(importDirective.GetString(), importDirective.GetStringLength());
}
importAbsPath.Append(importName);
rapidjson::Value patch;
ResultCode resolveResult = settings.m_importer->ResolveImport(&jsonDoc, patch, importDirective, importAbsPath, allocator);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
if ((settings.m_resolveFlags & ImportTracking::Imports) == ImportTracking::Imports)
{
rapidjson::Pointer path(element.Get().data(), element.Get().size());
settings.m_importer->AddImportDirective(path, importName);
}
if ((settings.m_resolveFlags & ImportTracking::Dependencies) == ImportTracking::Dependencies)
{
settings.m_importer->AddImportedFile(importAbsPath.String());
}
ResultCode result = ResolveNestedImports(jsonDoc, allocator, importPathStack, settings, importAbsPath, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
settings.m_importer->ApplyPatch(jsonDoc, patch, allocator);
}
else if (field.value.IsObject() || field.value.IsArray())
{
ScopedStackedString entryName(element, AZStd::string_view(field.name.GetString(), field.name.GetStringLength()));
ResultCode result = ResolveImports(field.value, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
}
else if(jsonDoc.IsArray())
{
int index = 0;
for (rapidjson::Value::ValueIterator elem = jsonDoc.Begin(); elem != jsonDoc.End(); ++elem, ++index)
{
if (!elem->IsObject() && !elem->IsArray())
{
continue;
}
ScopedStackedString entryName(element, index);
ResultCode result = ResolveImports(*elem, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject() || jsonDoc.IsArray())
{
const BaseJsonImporter::ImportDirectivesList& importDirectives = settings.m_importer->GetImportDirectives();
for (auto& import : importDirectives)
{
rapidjson::Pointer importPtr = import.first;
rapidjson::Value* currentValue = importPtr.Get(jsonDoc);
rapidjson::Value importedValue(rapidjson::kObjectType);
importedValue.AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(import.second.c_str()), allocator);
ResultCode resolveResult = JsonSerialization::ResolveImports(importedValue, allocator, settings);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
rapidjson::Value patch;
settings.m_importer->CreatePatch(patch, importedValue, *currentValue, allocator);
settings.m_importer->RestoreImport(currentValue, patch, allocator, import.second);
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
auto importedObject = JsonSerializationUtils::ReadJsonFile(importedFilePath.Native());
if (importedObject.IsSuccess())
{
rapidjson::Value& importedDoc = importedObject.GetValue();
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
}
else
{
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, const AZStd::string& importFilename)
{
using namespace JsonSerializationResult;
importPtr->SetObject();
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
rapidjson::Value importDirective(rapidjson::kObjectType);
importDirective.AddMember(rapidjson::StringRef("filename"), rapidjson::StringRef(importFilename.c_str()), allocator);
importDirective.AddMember(rapidjson::StringRef("patch"), patch, allocator);
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), importDirective, allocator);
}
else
{
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(importFilename.c_str()), allocator);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
return AZ::JsonSerialization::ApplyPatch(target, allocator, patch, JsonMergeApproach::JsonMergePatch);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator)
{
return JsonSerialization::CreatePatch(patch, allocator, source, target, JsonMergeApproach::JsonMergePatch);
}
void BaseJsonImporter::AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile)
{
m_importDirectives.emplace_back(jsonPtr, AZStd::move(importFile));
}
void BaseJsonImporter::AddImportedFile(AZStd::string importedFile)
{
m_importedFiles.insert(AZStd::move(importedFile));
}
const BaseJsonImporter::ImportDirectivesList& BaseJsonImporter::GetImportDirectives()
{
return m_importDirectives;
}
const BaseJsonImporter::ImportedFilesList& BaseJsonImporter::GetImportedFiles()
{
return m_importedFiles;
}
} // namespace AZ
@@ -0,0 +1,108 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include<AzCore/IO/Path/Path.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/pointer.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/Serialization/Json/JsonSerializationResult.h>
#include <AzCore/Serialization/Json/StackedString.h>
namespace AZ
{
struct JsonImportSettings;
class BaseJsonImporter
{
public:
AZ_RTTI(BaseJsonImporter, "{7B225807-7B43-430F-8B11-C794DCF5ACA5}");
using ImportDirectivesList = AZStd::vector<AZStd::pair<rapidjson::Pointer, AZStd::string>>;
using ImportedFilesList = AZStd::unordered_set<AZStd::string>;
virtual JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator,
const AZStd::string& importFilename);
virtual JsonSerializationResult::ResultCode ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator);
void AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile);
const ImportDirectivesList& GetImportDirectives();
void AddImportedFile(AZStd::string importedFile);
const ImportedFilesList& GetImportedFiles();
virtual ~BaseJsonImporter() = default;
protected:
ImportDirectivesList m_importDirectives;
ImportedFilesList m_importedFiles;
};
enum class ImportTracking : AZ::u8
{
None = 0,
Dependencies = (1<<0),
Imports = (1<<1),
All = (Dependencies | Imports)
};
AZ_DEFINE_ENUM_BITWISE_OPERATORS(ImportTracking);
class JsonImportResolver final
{
public:
using ImportPathStack = AZStd::vector<AZ::IO::FixedMaxPath>;
JsonImportResolver() = delete;
JsonImportResolver& operator=(const JsonImportResolver& rhs) = delete;
JsonImportResolver& operator=(JsonImportResolver&& rhs) = delete;
JsonImportResolver(const JsonImportResolver& rhs) = delete;
JsonImportResolver(JsonImportResolver&& rhs) = delete;
~JsonImportResolver() = delete;
static JsonSerializationResult::ResultCode ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element);
static JsonSerializationResult::ResultCode RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
static JsonSerializationResult::ResultCode ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element);
};
struct JsonImportSettings final
{
JsonSerializationResult::JsonIssueCallback m_reporting;
BaseJsonImporter* m_importer = nullptr;
ImportTracking m_resolveFlags = ImportTracking::All;
AZ::IO::FixedMaxPath m_loadedJsonPath;
};
} // namespace AZ
@@ -706,7 +706,7 @@ namespace AZ
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
}
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -717,6 +717,10 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
// Do an extra pass to find all the fields that are removed.
@@ -751,7 +755,7 @@ namespace AZ
rapidjson::Value value;
ResultCode result = CreateMergePatchInternal(value, allocator,
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -762,11 +766,20 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
if (target.MemberCount() == 0)
{
resultCode.Combine(settings.m_reporting("Added empty object to JSON Merge Patch.",
ResultCode(Tasks::CreatePatch, Outcomes::Success), element));
}
}
patch = AZStd::move(resultValue);
resultCode.Combine(ResultCode(Tasks::CreatePatch, Outcomes::Success));
return resultCode;
}
else
@@ -10,6 +10,7 @@
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/Json/BaseJsonSerializer.h>
#include <AzCore/Serialization/Json/JsonDeserializer.h>
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonMerger.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/JsonSerializer.h>
@@ -19,11 +20,6 @@
namespace AZ
{
const char* JsonSerialization::TypeIdFieldIdentifier = "$type";
const char* JsonSerialization::DefaultStringIdentifier = "{}";
const char* JsonSerialization::KeyFieldIdentifier = "Key";
const char* JsonSerialization::ValueFieldIdentifier = "Value";
namespace JsonSerializationInternal
{
template<typename T>
@@ -394,6 +390,60 @@ namespace AZ
}
}
JsonSerializationResult::ResultCode JsonSerialization::ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
JsonImportResolver::ImportPathStack importPathStack;
importPathStack.push_back(settings.m_loadedJsonPath);
StackedString element(StackedString::Format::JsonPointer);
return JsonImportResolver::ResolveImports(jsonDoc, allocator, importPathStack, settings, element);
}
JsonSerializationResult::ResultCode JsonSerialization::RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
settings.m_resolveFlags = ImportTracking::None;
return JsonImportResolver::RestoreImports(jsonDoc, allocator, settings);
}
JsonSerializationResult::ResultCode JsonSerialization::DefaultIssueReporter(AZStd::string& scratchBuffer,
AZStd::string_view message, JsonSerializationResult::ResultCode result, AZStd::string_view path)
{
@@ -18,6 +18,8 @@
namespace AZ
{
class BaseJsonSerializer;
struct JsonImportSettings;
enum class JsonMergeApproach
{
@@ -51,10 +53,11 @@ namespace AZ
class JsonSerialization final
{
public:
static const char* TypeIdFieldIdentifier;
static const char* DefaultStringIdentifier;
static const char* KeyFieldIdentifier;
static const char* ValueFieldIdentifier;
static constexpr const char* TypeIdFieldIdentifier = "$type";
static constexpr const char* DefaultStringIdentifier = "{}";
static constexpr const char* KeyFieldIdentifier = "Key";
static constexpr const char* ValueFieldIdentifier = "Value";
static constexpr const char* ImportDirectiveIdentifier = "$import";
//! Merges two json values together by applying "patch" to "target" using the selected merge algorithm.
//! This version of ApplyPatch is destructive to "target". If the patch can't be correctly applied it will
@@ -284,6 +287,22 @@ namespace AZ
//! @return An enum containing less, equal or greater. In case of an error, the value for the enum will "error".
static JsonSerializerCompareResult Compare(const rapidjson::Value& lhs, const rapidjson::Value& rhs);
//! Resolves all import directives, including nested imports, in the given document. An importer object needs to be passed
//! in through the settings.
//! @param jsonDoc The json document in which to resolve imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are resolved.
static JsonSerializationResult::ResultCode ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
//! Restores all import directives that were present in the json document. The same importer object that was
//! passed into ResolveImports through the settings needs to be passed here through settings as well.
//! @param jsonDoc The json document in which to restore imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are restored.
static JsonSerializationResult::ResultCode RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
JsonSerialization() = delete;
~JsonSerialization() = delete;
@@ -69,6 +69,9 @@ namespace AZ
case Tasks::CreatePatch:
target.append("a create patch operation ");
break;
case Tasks::Import:
target.append("an import operation");
break;
default:
target.append("an unknown operation ");
break;
@@ -32,7 +32,8 @@ namespace AZ
ReadField, //!< Task to read a field from JSON to a value.
WriteValue, //!< Task to write a value to a JSON field.
Merge, //!< Task to merge two JSON values/documents together.
CreatePatch //!< Task to create a patch to transform one value/document to another.
CreatePatch, //!< Task to create a patch to transform one value/document to another.
Import //!< Task to import a JSON document.
};
//! Describes how the task was processed.
+1 -1
View File
@@ -120,7 +120,7 @@ namespace AZ::Utils
AZ::Outcome<void, AZStd::string> WriteFile(AZStd::string_view content, AZStd::string_view filePath)
{
AZ::IO::FixedMaxPath filePathFixed = filePath; // Because FileIOStream requires a null-terminated string
AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite);
AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath);
bool success = false;
@@ -522,6 +522,8 @@ set(FILES
Serialization/Json/IntSerializer.cpp
Serialization/Json/JsonDeserializer.h
Serialization/Json/JsonDeserializer.cpp
Serialization/Json/JsonImporter.cpp
Serialization/Json/JsonImporter.h
Serialization/Json/JsonMerger.h
Serialization/Json/JsonMerger.cpp
Serialization/Json/JsonSerialization.h
@@ -0,0 +1,413 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <Tests/Serialization/Json/JsonSerializationTests.h>
namespace JsonSerializationTests
{
class JsonImportingTests;
class JsonImporterCustom
: public AZ::BaseJsonImporter
{
public:
AZ_RTTI(JsonImporterCustom, "{003F5896-71E0-4A50-A14F-08C319B06AD0}");
AZ::JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) override;
JsonImporterCustom(JsonImportingTests* tests)
{
testClass = tests;
}
private:
JsonImportingTests* testClass;
};
class JsonImportingTests
: public BaseJsonSerializerFixture
{
public:
void SetUp() override
{
BaseJsonSerializerFixture::SetUp();
}
void TearDown() override
{
BaseJsonSerializerFixture::TearDown();
}
void GetTestDocument(const AZStd::string& docName, rapidjson::Document& out)
{
const char *objectJson = R"({
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
})";
const char *arrayJson = R"([
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
])";
const char *nestedImportJson = R"({
"desc" : "Nested Import",
"obj" : {"$import" : "object.json"}
})";
const char *nestedImportCycle1Json = R"({
"desc" : "Nested Import Cycle 1",
"obj" : {"$import" : "nested_import_c2.json"}
})";
const char *nestedImportCycle2Json = R"({
"desc" : "Nested Import Cycle 2",
"obj" : {"$import" : "nested_import_c1.json"}
})";
if (docName.compare("object.json") == 0)
{
out.Parse(objectJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("array.json") == 0)
{
out.Parse(arrayJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import.json") == 0)
{
out.Parse(nestedImportJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c1.json") == 0)
{
out.Parse(nestedImportCycle1Json);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c2.json") == 0)
{
out.Parse(nestedImportCycle2Json);
ASSERT_FALSE(out.HasParseError());
}
}
protected:
void TestImportLoadStore(const char* input, const char* expectedImportedValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
rapidjson::Document expectedOutcome;
expectedOutcome.Parse(expectedImportedValue);
ASSERT_FALSE(expectedOutcome.HasParseError());
TestResolveImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutcome.GetObject());
rapidjson::Document originalInput;
originalInput.Parse(input);
ASSERT_FALSE(originalInput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), originalInput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
void TestImportCycle(const char* input)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
AZ::JsonSerializationResult::ResultCode result = TestResolveImports(importerObj);
EXPECT_EQ(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Catastrophic);
m_jsonDocument->SetObject();
delete importerObj;
}
void TestInsertNewImport(const char* input, const char* expectedRestoredValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
TestResolveImports(importerObj);
importerObj->AddImportDirective(rapidjson::Pointer("/object_2"), "object.json");
rapidjson::Document expectedOutput;
expectedOutput.Parse(expectedRestoredValue);
ASSERT_FALSE(expectedOutput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
AZ::JsonSerializationResult::ResultCode TestResolveImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::ResolveImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
AZ::JsonSerializationResult::ResultCode TestRestoreImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::RestoreImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
};
AZ::JsonSerializationResult::ResultCode JsonImporterCustom::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective, const AZ::IO::FixedMaxPath& importedFilePath,
rapidjson::Document::AllocatorType& allocator)
{
AZ::JsonSerializationResult::ResultCode resultCode(AZ::JsonSerializationResult::Tasks::Import);
rapidjson::Document importedDoc;
testClass->GetTestDocument(importedFilePath.String(), importedDoc);
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
return resultCode;
}
// Test Cases
TEST_F(JsonImportingTests, ImportSimpleObjectTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {"$import" : "object.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleObjectPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_2" : "patched_value" }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "patched_value",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {"$import" : "array.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {
"$import" : {
"filename" : "array.json",
"patch" : [ { "element_1" : "patched_value" } ]
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "patched_value" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {"$import" : "nested_import.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportPatchTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {
"$import" : {
"filename" : "nested_import.json",
"patch" : { "obj" : { "field_3" : "patched_value" } }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "patched_value"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportCycleTest)
{
const char* inputFile = R"(
{
"name" : "nested_import_cycle",
"object": {"$import" : "nested_import_c1.json"}
}
)";
TestImportCycle(inputFile);
}
TEST_F(JsonImportingTests, InsertNewImportTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"field_1" : "other_value",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_1" : "other_value" }
}
}
}
)";
TestInsertNewImport(inputFile, expectedOutput);
}
}
@@ -121,6 +121,7 @@ set(FILES
Serialization/Json/TestCases_Classes.cpp
Serialization/Json/TestCases_Compare.cpp
Serialization/Json/TestCases_Enum.cpp
Serialization/Json/TestCases_Importing.cpp
Serialization/Json/TestCases_Patching.cpp
Serialization/Json/TestCases_Pointers.h
Serialization/Json/TestCases_Pointers.cpp
@@ -58,6 +58,7 @@
#include <AzFramework/Script/ScriptComponent.h>
#include <AzFramework/Spawnable/SpawnableSystemComponent.h>
#include <AzFramework/StreamingInstall/StreamingInstall.h>
#include <AzFramework/SurfaceData/SurfaceData.h>
#include <AzFramework/TargetManagement/TargetManagementComponent.h>
#include <AzFramework/Viewport/CameraState.h>
#include <AzFramework/Metrics/MetricsPlainTextNameRegistration.h>
@@ -278,6 +279,8 @@ namespace AzFramework
AzFramework::RemoteStorageDriveConfig::Reflect(context);
Physics::ReflectionUtils::ReflectPhysicsApi(context);
AzFramework::SurfaceData::SurfaceTagWeight::Reflect(context);
AzFramework::SurfaceData::SurfacePoint::Reflect(context);
AzFramework::Terrain::TerrainDataRequests::Reflect(context);
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
@@ -43,7 +43,7 @@ namespace AzFramework
class IMatchmakingAsyncRequests
{
public:
AZ_RTTI(ISessionAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}");
AZ_RTTI(IMatchmakingAsyncRequests, "{53513480-2D02-493C-B44E-96AA27F42429}");
IMatchmakingAsyncRequests() = default;
virtual ~IMatchmakingAsyncRequests() = default;
@@ -60,4 +60,31 @@ namespace AzFramework
// @param stopMatchmakingRequest The request of StopMatchmaking operation
virtual void StopMatchmakingAsync(const StopMatchmakingRequest& stopMatchmakingRequest) = 0;
};
//! MatchmakingAsyncRequestNotifications
//! The notifications correspond to matchmaking async requests
class MatchmakingAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes
virtual void OnAcceptMatchAsyncComplete() = 0;
// OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes
// @param matchmakingTicketId The unique identifier for the matchmaking ticket
virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0;
// OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes
virtual void OnStopMatchmakingAsyncComplete() = 0;
};
using MatchmakingAsyncRequestNotificationBus = AZ::EBus<MatchmakingAsyncRequestNotifications>;
} // namespace AzFramework
@@ -13,36 +13,10 @@
namespace AzFramework
{
//! MatchmakingAsyncRequestNotifications
//! The notifications correspond to matchmaking async requests
class MatchmakingAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
// Safeguard handler for multi-threaded use case
using MutexType = AZStd::recursive_mutex;
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnAcceptMatchAsyncComplete is fired once AcceptMatchAsync completes
virtual void OnAcceptMatchAsyncComplete() = 0;
// OnStartMatchmakingAsyncComplete is fired once StartMatchmakingAsync completes
// @param matchmakingTicketId The unique identifier for the matchmaking ticket
virtual void OnStartMatchmakingAsyncComplete(const AZStd::string& matchmakingTicketId) = 0;
// OnStopMatchmakingAsyncComplete is fired once StopMatchmakingAsync completes
virtual void OnStopMatchmakingAsyncComplete() = 0;
};
using MatchmakingAsyncRequestNotificationBus = AZ::EBus<MatchmakingAsyncRequestNotifications>;
//! MatchmakingNotifications
//! The matchmaking notifications to listen for performing required operations
class MatchAcceptanceNotifications
//! based on matchmaking ticket event
class MatchmakingNotifications
: public AZ::EBusTraits
{
public:
@@ -55,8 +29,18 @@ namespace AzFramework
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnMatchAcceptance is fired when DescribeMatchmaking ticket status is REQUIRES_ACCEPTANCE
// OnMatchAcceptance is fired when match is found and pending on acceptance
// Use this notification to accept found match
virtual void OnMatchAcceptance() = 0;
// OnMatchComplete is fired when match is complete
virtual void OnMatchComplete() = 0;
// OnMatchError is fired when match is processed with error
virtual void OnMatchError() = 0;
// OnMatchFailure is fired when match is failed to complete
virtual void OnMatchFailure() = 0;
};
using MatchAcceptanceNotificationBus = AZ::EBus<MatchAcceptanceNotifications>;
using MatchmakingNotificationBus = AZ::EBus<MatchmakingNotifications>;
} // namespace AzFramework
@@ -0,0 +1,97 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Math/Vector3.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/Component/ComponentBus.h>
#include <AzCore/Math/Aabb.h>
#include <AzFramework/Physics/Material.h>
namespace Physics
{
//! The QuadMeshType specifies the property of the heightfield quad.
enum class QuadMeshType : uint8_t
{
SubdivideUpperLeftToBottomRight, //!< Subdivide the quad, from upper left to bottom right |\|, into two triangles.
SubdivideBottomLeftToUpperRight, //!< Subdivide the quad, from bottom left to upper right |/|, into two triangles.
Hole //!< The quad should be treated as a hole in the heightfield.
};
struct HeightMaterialPoint
{
float m_height{ 0.0f }; //!< Holds the height of this point in the heightfield relative to the heightfield entity location.
QuadMeshType m_quadMeshType{ QuadMeshType::SubdivideUpperLeftToBottomRight }; //!< By default, create two triangles like this |\|, where this point is in the upper left corner.
uint8_t m_materialIndex{ 0 }; //!< The surface material index for the upper left corner of this quad.
uint16_t m_padding{ 0 }; //!< available for future use.
};
//! An interface to provide heightfield values.
class HeightfieldProviderRequests
: public AZ::ComponentBus
{
public:
//! Returns the distance between each height in the map.
//! @return Vector containing Column Spacing, Rows Spacing.
virtual AZ::Vector2 GetHeightfieldGridSpacing() const = 0;
//! Returns the height field gridsize.
//! @param numColumns contains the size of the grid in the x direction.
//! @param numRows contains the size of the grid in the y direction.
virtual void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const = 0;
//! Returns the height field min and max height bounds.
//! @param minHeightBounds contains the minimum height that the heightfield can contain.
//! @param maxHeightBounds contains the maximum height that the heightfield can contain.
virtual void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const = 0;
//! Returns the AABB of the heightfield.
//! This is provided separately from the shape AABB because the heightfield might choose to modify the AABB bounds.
//! @return AABB of the heightfield.
virtual AZ::Aabb GetHeightfieldAabb() const = 0;
//! Returns the world transform for the heightfield.
//! This is provided separately from the entity transform because the heightfield might want to clear out the rotation or scale.
//! @return world transform that should be used with the heightfield data.
virtual AZ::Transform GetHeightfieldTransform() const = 0;
//! Returns the list of materials used by the height field.
//! @return returns a vector of all materials.
virtual AZStd::vector<MaterialId> GetMaterialList() const = 0;
//! Returns the list of heights used by the height field.
//! @return the rows*columns vector of the heights.
virtual AZStd::vector<float> GetHeights() const = 0;
//! Returns the list of heights and materials used by the height field.
//! @return the rows*columns vector of the heights and materials.
virtual AZStd::vector<Physics::HeightMaterialPoint> GetHeightsAndMaterials() const = 0;
};
using HeightfieldProviderRequestsBus = AZ::EBus<HeightfieldProviderRequests>;
//! Broadcasts notifications when heightfield data changes - heightfield providers implement HeightfieldRequests bus.
class HeightfieldProviderNotifications
: public AZ::ComponentBus
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
//! Called whenever the heightfield data changes.
//! @param the AABB of the area of data that changed.
virtual void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion)
{
}
protected:
~HeightfieldProviderNotifications() = default;
};
using HeightfieldProviderNotificationBus = AZ::EBus<HeightfieldProviderNotifications>;
} // namespace Physics
@@ -0,0 +1,33 @@
/*
* 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 <gmock/gmock.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
namespace UnitTest
{
class MockHeightfieldProviderNotificationBusListener
: private Physics::HeightfieldProviderNotificationBus::Handler
{
public:
MockHeightfieldProviderNotificationBusListener(AZ::EntityId entityid)
{
Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityid);
}
~MockHeightfieldProviderNotificationBusListener()
{
Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect();
}
MOCK_METHOD1(OnHeightfieldDataChanged, void(const AZ::Aabb&));
};
} // namespace UnitTest
@@ -37,6 +37,7 @@ namespace Physics
REFLECT_SHAPETYPE_ENUM_VALUE(Sphere);
REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder);
REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset);
REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield);
#undef REFLECT_SHAPETYPE_ENUM_VALUE
}
@@ -285,12 +286,17 @@ namespace Physics
return m_type;
}
void* CookedMeshShapeConfiguration::GetCachedNativeMesh() const
const void* CookedMeshShapeConfiguration::GetCachedNativeMesh() const
{
return m_cachedNativeMesh;
}
void CookedMeshShapeConfiguration::SetCachedNativeMesh(void* cachedNativeMesh) const
void* CookedMeshShapeConfiguration::GetCachedNativeMesh()
{
return m_cachedNativeMesh;
}
void CookedMeshShapeConfiguration::SetCachedNativeMesh(void* cachedNativeMesh)
{
m_cachedNativeMesh = cachedNativeMesh;
}
@@ -305,4 +311,131 @@ namespace Physics
m_cachedNativeMesh = nullptr;
}
}
}
void HeightfieldShapeConfiguration::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext
->RegisterGenericType<AZStd::shared_ptr<HeightfieldShapeConfiguration>>();
serializeContext->Class<HeightfieldShapeConfiguration, ShapeConfiguration>()
->Version(1);
}
}
HeightfieldShapeConfiguration::~HeightfieldShapeConfiguration()
{
SetCachedNativeHeightfield(nullptr);
}
HeightfieldShapeConfiguration::HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration& other)
: ShapeConfiguration(other)
, m_gridResolution(other.m_gridResolution)
, m_numColumns(other.m_numColumns)
, m_numRows(other.m_numRows)
, m_samples(other.m_samples)
, m_minHeightBounds(other.m_minHeightBounds)
, m_maxHeightBounds(other.m_maxHeightBounds)
, m_cachedNativeHeightfield(nullptr)
{
}
HeightfieldShapeConfiguration& HeightfieldShapeConfiguration::operator=(const HeightfieldShapeConfiguration& other)
{
ShapeConfiguration::operator=(other);
m_gridResolution = other.m_gridResolution;
m_numColumns = other.m_numColumns;
m_numRows = other.m_numRows;
m_samples = other.m_samples;
m_minHeightBounds = other.m_minHeightBounds;
m_maxHeightBounds = other.m_maxHeightBounds;
// Prevent raw pointer from being copied
m_cachedNativeHeightfield = nullptr;
return *this;
}
const void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield() const
{
return m_cachedNativeHeightfield;
}
void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield()
{
return m_cachedNativeHeightfield;
}
void HeightfieldShapeConfiguration::SetCachedNativeHeightfield(void* cachedNativeHeightfield)
{
if (m_cachedNativeHeightfield)
{
Physics::SystemRequestBus::Broadcast(&Physics::SystemRequests::ReleaseNativeHeightfieldObject, m_cachedNativeHeightfield);
}
m_cachedNativeHeightfield = cachedNativeHeightfield;
}
AZ::Vector2 HeightfieldShapeConfiguration::GetGridResolution() const
{
return m_gridResolution;
}
void HeightfieldShapeConfiguration::SetGridResolution(const AZ::Vector2& gridResolution)
{
m_gridResolution = gridResolution;
}
int32_t HeightfieldShapeConfiguration::GetNumColumns() const
{
return m_numColumns;
}
void HeightfieldShapeConfiguration::SetNumColumns(int32_t numColumns)
{
m_numColumns = numColumns;
}
int32_t HeightfieldShapeConfiguration::GetNumRows() const
{
return m_numRows;
}
void HeightfieldShapeConfiguration::SetNumRows(int32_t numRows)
{
m_numRows = numRows;
}
const AZStd::vector<Physics::HeightMaterialPoint>& HeightfieldShapeConfiguration::GetSamples() const
{
return m_samples;
}
void HeightfieldShapeConfiguration::SetSamples(const AZStd::vector<Physics::HeightMaterialPoint>& samples)
{
m_samples = samples;
}
float HeightfieldShapeConfiguration::GetMinHeightBounds() const
{
return m_minHeightBounds;
}
void HeightfieldShapeConfiguration::SetMinHeightBounds(float minBounds)
{
m_minHeightBounds = minBounds;
}
float HeightfieldShapeConfiguration::GetMaxHeightBounds() const
{
return m_maxHeightBounds;
}
void HeightfieldShapeConfiguration::SetMaxHeightBounds(float maxBounds)
{
m_maxHeightBounds = maxBounds;
}
} // namespace Physics
@@ -9,10 +9,13 @@
#pragma once
#include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Quaternion.h>
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
namespace Physics
{
/// Used to identify shape configuration type from base class.
@@ -27,6 +30,7 @@ namespace Physics
Native, ///< Native shape configuration if user wishes to bypass generic shape configurations.
PhysicsAsset, ///< Shapes configured in the asset.
CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine.
Heightfield ///< Interacts with the physics system heightfield
};
class ShapeConfiguration
@@ -183,8 +187,9 @@ namespace Physics
MeshType GetMeshType() const;
void* GetCachedNativeMesh() const;
void SetCachedNativeMesh(void* cachedNativeMesh) const;
void* GetCachedNativeMesh();
const void* GetCachedNativeMesh() const;
void SetCachedNativeMesh(void* cachedNativeMesh);
private:
void ReleaseCachedNativeMesh();
@@ -193,7 +198,56 @@ namespace Physics
MeshType m_type = MeshType::TriangleMesh;
//! Cached native mesh object (e.g. PxConvexMesh or PxTriangleMesh). This data is not serialized.
mutable void* m_cachedNativeMesh = nullptr;
void* m_cachedNativeMesh = nullptr;
};
class HeightfieldShapeConfiguration
: public ShapeConfiguration
{
public:
AZ_CLASS_ALLOCATOR(HeightfieldShapeConfiguration, AZ::SystemAllocator, 0);
AZ_RTTI(HeightfieldShapeConfiguration, "{8DF47C83-D2A9-4E7C-8620-5E173E43C0B3}", ShapeConfiguration);
static void Reflect(AZ::ReflectContext* context);
HeightfieldShapeConfiguration() = default;
HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration&);
HeightfieldShapeConfiguration& operator=(const HeightfieldShapeConfiguration&);
~HeightfieldShapeConfiguration();
ShapeType GetShapeType() const override
{
return ShapeType::Heightfield;
}
const void* GetCachedNativeHeightfield() const;
void* GetCachedNativeHeightfield();
void SetCachedNativeHeightfield(void* cachedNativeHeightfield);
AZ::Vector2 GetGridResolution() const;
void SetGridResolution(const AZ::Vector2& gridSpacing);
int32_t GetNumColumns() const;
void SetNumColumns(int32_t numColumns);
int32_t GetNumRows() const;
void SetNumRows(int32_t numRows);
const AZStd::vector<Physics::HeightMaterialPoint>& GetSamples() const;
void SetSamples(const AZStd::vector<Physics::HeightMaterialPoint>& samples);
float GetMinHeightBounds() const;
void SetMinHeightBounds(float minBounds);
float GetMaxHeightBounds() const;
void SetMaxHeightBounds(float maxBounds);
private:
//! The number of meters between each heightfield sample.
AZ::Vector2 m_gridResolution{ 1.0f };
//! The number of columns in the heightfield sample grid.
int32_t m_numColumns{ 0 };
//! The number of rows in the heightfield sample grid.
int32_t m_numRows{ 0 };
//! The minimum and maximum heights that can be used by this heightfield.
//! This can be used by the physics system to choose a more optimal heightfield data type internally (ex: int16, uint8)
float m_minHeightBounds{AZStd::numeric_limits<float>::lowest()};
float m_maxHeightBounds{AZStd::numeric_limits<float>::max()};
//! The grid of sample points for the heightfield.
AZStd::vector<Physics::HeightMaterialPoint> m_samples;
//! An optional storage pointer for the physics system to cache its native heightfield representation.
void* m_cachedNativeHeightfield{ nullptr };
};
} // namespace Physics
@@ -132,6 +132,10 @@ namespace Physics
virtual AZStd::shared_ptr<Material> CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) = 0;
/// Releases the height field object created by the physics backend.
/// @param nativeHeightfieldObject Pointer to the height field object.
virtual void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) = 0;
/// Releases the mesh object created by the physics backend.
/// @param nativeMeshObject Pointer to the mesh object.
virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0;
@@ -107,6 +107,7 @@ namespace Physics
PhysicsAssetShapeConfiguration::Reflect(context);
NativeShapeConfiguration::Reflect(context);
CookedMeshShapeConfiguration::Reflect(context);
HeightfieldShapeConfiguration::Reflect(context);
AzPhysics::SystemInterface::Reflect(context);
AzPhysics::Scene::Reflect(context);
AzPhysics::CollisionLayer::Reflect(context);
@@ -0,0 +1,11 @@
#
# 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
#
#
set(FILES
Mocks/MockHeightfieldProviderBus.h
)
@@ -22,7 +22,7 @@ namespace AzFramework
AZStd::scoped_ptr<ProcessWatcher> pWatcher(LaunchProcess(processLaunchInfo, communicationType));
if (!pWatcher)
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -31,7 +31,7 @@ namespace AzFramework
ProcessCommunicator* pCommunicator = pWatcher->GetCommunicator();
if (!pCommunicator || !pCommunicator->IsValid())
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -0,0 +1,59 @@
/*
* 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 <AzFramework/SurfaceData/SurfaceData.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
namespace AzFramework::SurfaceData
{
void SurfaceTagWeight::Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<SurfaceTagWeight>()
->Field("m_surfaceType", &SurfaceTagWeight::m_surfaceType)
->Field("m_weight", &SurfaceTagWeight::m_weight)
;
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<SurfaceTagWeight>()
->Attribute(AZ::Script::Attributes::Category, "SurfaceData")
->Constructor()
->Property("surfaceType", BehaviorValueProperty(&SurfaceTagWeight::m_surfaceType))
->Property("weight", BehaviorValueProperty(&SurfaceTagWeight::m_weight))
;
}
}
void SurfacePoint::Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<SurfacePoint>()
->Field("m_position", &SurfacePoint::m_position)
->Field("m_normal", &SurfacePoint::m_normal)
->Field("m_surfaceTags", &SurfacePoint::m_surfaceTags)
;
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<SurfacePoint>("AzFramework::SurfaceData::SurfacePoint")
->Attribute(AZ::Script::Attributes::Category, "SurfaceData")
->Constructor()
->Property("position", BehaviorValueProperty(&SurfacePoint::m_position))
->Property("normal", BehaviorValueProperty(&SurfacePoint::m_normal))
->Property("surfaceTags", BehaviorValueProperty(&SurfacePoint::m_surfaceTags))
;
}
}
} // namespace AzFramework::SurfaceData
@@ -0,0 +1,72 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Math/Crc.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/std/containers/vector.h>
namespace AzFramework::SurfaceData
{
namespace Constants
{
static constexpr const char* s_unassignedTagName = "(unassigned)";
}
struct SurfaceTagWeight
{
AZ_TYPE_INFO(SurfaceTagWeight, "{EA14018E-E853-4BF5-8E13-D83BB99A54CC}");
SurfaceTagWeight() = default;
SurfaceTagWeight(AZ::Crc32 surfaceType, float weight)
: m_surfaceType(surfaceType)
, m_weight(weight)
{
}
AZ::Crc32 m_surfaceType = AZ::Crc32(Constants::s_unassignedTagName);
float m_weight = 0.0f; //! A Value in the range [0.0f .. 1.0f]
static void Reflect(AZ::ReflectContext* context);
};
struct SurfaceTagWeightComparator
{
bool operator()(const SurfaceTagWeight& tagWeight1, const SurfaceTagWeight& tagWeight2) const
{
// Return a deterministic sort order for surface tags from highest to lowest weight, with the surface types sorted
// in a predictable order when the weights are equal. The surface type sort order is meaningless since it is sorting CRC
// values, it's really just important for it to be stable.
// For the floating-point weight comparisons we use exact instead of IsClose value comparisons for a similar reason - we
// care about being sorted highest to lowest, but there's no inherent meaning in sorting surface types with *similar* weights
// together.
if (tagWeight1.m_weight != tagWeight2.m_weight)
{
return tagWeight1.m_weight > tagWeight2.m_weight;
}
else
{
return tagWeight1.m_surfaceType > tagWeight2.m_surfaceType;
}
}
};
using SurfaceTagWeightList = AZStd::vector<SurfaceTagWeight>;
struct SurfacePoint final
{
AZ_TYPE_INFO(SurfacePoint, "{331A3D0E-BB1D-47BF-96A2-249FAA0D720D}");
AZ::Vector3 m_position;
AZ::Vector3 m_normal;
SurfaceTagWeightList m_surfaceTags;
static void Reflect(AZ::ReflectContext* context);
};
} // namespace AzFramework::SurfaceData
@@ -8,56 +8,33 @@
#include "TerrainDataRequestBus.h"
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
namespace AzFramework
namespace AzFramework::Terrain
{
namespace SurfaceData
void TerrainDataRequests::Reflect(AZ::ReflectContext* context)
{
void SurfaceTagWeight::Reflect(AZ::ReflectContext* context)
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<SurfaceTagWeight>()
->Field("m_surfaceType", &SurfaceTagWeight::m_surfaceType)
->Field("m_weight", &SurfaceTagWeight::m_weight)
;
}
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->Class<SurfaceTagWeight>("SurfaceTagWeight")
->Property("m_surfaceType", BehaviorValueProperty(&SurfaceTagWeight::m_surfaceType))
->Property("m_weight", BehaviorValueProperty(&SurfaceTagWeight::m_weight))
;
}
}
} //namespace SurfaceData
namespace Terrain
{
void TerrainDataRequests::Reflect(AZ::ReflectContext* context)
{
AzFramework::SurfaceData::SurfaceTagWeight::Reflect(context);
if (AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->EBus<AzFramework::Terrain::TerrainDataRequestBus>("TerrainDataRequestBus")
->Attribute(AZ::Script::Attributes::Category, "Terrain")
->Event("GetHeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetHeight)
->Event("GetHeightFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetHeightFromFloats)
->Event("GetMaxSurfaceWeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeight)
->Event("GetMaxSurfaceWeightFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeightFromFloats)
->Event("GetIsHoleFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetIsHoleFromFloats)
->Event("GetNormal", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetNormal)
->Event("GetNormalFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetNormalFromFloats)
->Event("GetTerrainAabb", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb)
->Event("GetTerrainHeightQueryResolution",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightQueryResolution)
;
}
behaviorContext->EBus<AzFramework::Terrain::TerrainDataRequestBus>("TerrainDataRequestBus")
->Attribute(AZ::Script::Attributes::Category, "Terrain")
->Event("GetHeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetHeight)
->Event("GetNormal", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetNormal)
->Event("GetMaxSurfaceWeight", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeight)
->Event("GetMaxSurfaceWeightFromVector2",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetMaxSurfaceWeightFromVector2)
->Event("GetSurfaceWeights", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfaceWeights)
->Event("GetSurfaceWeightsFromVector2",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfaceWeightsFromVector2)
->Event("GetIsHoleFromFloats", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetIsHoleFromFloats)
->Event("GetSurfacePoint", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfacePoint)
->Event("GetSurfacePointFromVector2",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetSurfacePointFromVector2)
->Event("GetTerrainAabb", &AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainAabb)
->Event("GetTerrainHeightQueryResolution",
&AzFramework::Terrain::TerrainDataRequestBus::Events::GetTerrainHeightQueryResolution)
;
}
} //namespace Terrain
} // namespace AzFramework
}
} // namespace AzFramework::Terrain
@@ -8,50 +8,13 @@
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/std/containers/set.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Aabb.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzFramework/SurfaceData/SurfaceData.h>
namespace AzFramework
{
namespace SurfaceData
{
namespace Constants
{
static const char* s_unassignedTagName = "(unassigned)";
}
struct SurfaceTagWeight
{
AZ_TYPE_INFO(SurfaceTagWeight, "{EA14018E-E853-4BF5-8E13-D83BB99A54CC}");
AZ::Crc32 m_surfaceType = AZ::Crc32(Constants::s_unassignedTagName);
float m_weight = 0.0f; //! A Value in the range [0.0f .. 1.0f]
//! Don't call this directly. TerrainDataRequests::Reflect is doing it already.
static void Reflect(AZ::ReflectContext* context);
};
struct SurfaceTagWeightComparator
{
bool operator()(const SurfaceTagWeight& tagWeight1, const SurfaceTagWeight& tagWeight2) const
{
if (!AZ::IsClose(tagWeight1.m_weight, tagWeight2.m_weight))
{
return tagWeight1.m_weight > tagWeight2.m_weight;
}
else
{
return tagWeight1.m_surfaceType > tagWeight2.m_surfaceType;
}
}
};
using OrderedSurfaceTagWeightSet = AZStd::set<SurfaceTagWeight, SurfaceTagWeightComparator>;
} //namespace SurfaceData
namespace Terrain
{
@@ -91,49 +54,82 @@ namespace AzFramework
//! Returns terrains height in meters at location x,y.
//! @terrainExistsPtr: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will become false,
//! otherwise *terrainExistsPtr will become true.
virtual float GetHeight(AZ::Vector3 position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual float GetHeightFromFloats(float x, float y, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual float GetHeight(const AZ::Vector3& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual float GetHeightFromVector2(
const AZ::Vector2& position, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual float GetHeightFromFloats(
float x, float y, Sampler sampler = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Returns true if there's a hole at location x,y.
//! Also returns true if there's no terrain data at location x,y.
virtual bool GetIsHole(const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR) const = 0;
virtual bool GetIsHoleFromVector2(const AZ::Vector2& position, Sampler sampleFilter = Sampler::BILINEAR) const = 0;
virtual bool GetIsHoleFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR) const = 0;
// Given an XY coordinate, return the surface normal.
//! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a
//! terrain HOLE then *terrainExistsPtr will be set to false,
//! otherwise *terrainExistsPtr will be set to true.
virtual AZ::Vector3 GetNormal(
const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual AZ::Vector3 GetNormalFromVector2(
const AZ::Vector2& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual AZ::Vector3 GetNormalFromFloats(
float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Given an XY coordinate, return the max surface type and weight.
//! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will be set to false,
//! otherwise *terrainExistsPtr will be set to true.
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeight(AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromVector2(const AZ::Vector2& inPosition, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0;
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeight(
const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromVector2(
const AZ::Vector2& inPosition, Sampler sampleFilter = Sampler::DEFAULT, bool* terrainExistsPtr = nullptr) const = 0;
virtual SurfaceData::SurfaceTagWeight GetMaxSurfaceWeightFromFloats(
float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Given an XY coordinate, return the set of surface types and weights. The Vector3 input position version is defined to ignore
//! the input Z value.
virtual void GetSurfaceWeights(
const AZ::Vector3& inPosition,
SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights,
SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
virtual void GetSurfaceWeightsFromVector2(
const AZ::Vector2& inPosition,
SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights,
SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
virtual void GetSurfaceWeightsFromFloats(
float x,
float y,
SurfaceData::OrderedSurfaceTagWeightSet& outSurfaceWeights,
SurfaceData::SurfaceTagWeightList& outSurfaceWeights,
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
//! Convenience function for low level systems that can't do a reverse lookup from Crc to string. Everyone else should use GetMaxSurfaceWeight or GetMaxSurfaceWeightFromFloats.
//! Not available in the behavior context.
//! Returns nullptr if the position is inside a hole or outside of the terrain boundaries.
virtual const char * GetMaxSurfaceName(AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual const char* GetMaxSurfaceName(
const AZ::Vector3& position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Returns true if there's a hole at location x,y.
//! Also returns true if there's no terrain data at location x,y.
virtual bool GetIsHoleFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR) const = 0;
// Given an XY coordinate, return the surface normal.
//! @terrainExists: Can be nullptr. If != nullptr then, if there's no terrain at location x,y or location x,y is inside a terrain HOLE then *terrainExistsPtr will be set to false,
//! otherwise *terrainExistsPtr will be set to true.
virtual AZ::Vector3 GetNormal(AZ::Vector3 position, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
virtual AZ::Vector3 GetNormalFromFloats(float x, float y, Sampler sampleFilter = Sampler::BILINEAR, bool* terrainExistsPtr = nullptr) const = 0;
//! Given an XY coordinate, return all terrain information at that location. The Vector3 input position version is defined
//! to ignore the input Z value.
virtual void GetSurfacePoint(
const AZ::Vector3& inPosition,
SurfaceData::SurfacePoint& outSurfacePoint,
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
virtual void GetSurfacePointFromVector2(
const AZ::Vector2& inPosition,
SurfaceData::SurfacePoint& outSurfacePoint,
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
virtual void GetSurfacePointFromFloats(
float x,
float y,
SurfaceData::SurfacePoint& outSurfacePoint,
Sampler sampleFilter = Sampler::DEFAULT,
bool* terrainExistsPtr = nullptr) const = 0;
};
using TerrainDataRequestBus = AZ::EBus<TerrainDataRequests>;
@@ -169,6 +165,10 @@ namespace AzFramework
}
};
using TerrainDataNotificationBus = AZ::EBus<TerrainDataNotifications>;
} //namespace Terrain
} // namespace Terrain
} // namespace AzFramework
namespace AZ
{
AZ_TYPE_INFO_SPECIALIZE(AzFramework::Terrain::TerrainDataRequests::Sampler, "{D29BB6D7-3006-4114-858D-355EAA256B86}");
} // namespace AZ
@@ -14,9 +14,8 @@ namespace AzFramework
{
AZ_CVAR(bool, bg_octreeUseQuadtree, false, nullptr, AZ::ConsoleFunctorFlags::ReadOnly, "If set to true, the visibility octrees will degenerate to a quadtree split along the X/Y plane");
AZ_CVAR(float, bg_octreeMaxWorldExtents, 16384.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum supported world size by the world octreeSystemComponent");
AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split");
AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation");
AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split");
AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation");
static uint32_t GetChildNodeCount()
{
@@ -25,14 +24,12 @@ namespace AzFramework
return (bg_octreeUseQuadtree) ? QuadtreeNodeChildCount : OctreeNodeChildCount;
}
OctreeNode::OctreeNode(const AZ::Aabb& bounds)
: m_bounds(bounds)
{
;
}
OctreeNode::OctreeNode(OctreeNode&& rhs)
: m_bounds(rhs.m_bounds)
, m_parent(rhs.m_parent)
@@ -46,7 +43,6 @@ namespace AzFramework
}
}
OctreeNode& OctreeNode::operator=(OctreeNode&& rhs)
{
m_bounds = rhs.m_bounds;
@@ -63,7 +59,6 @@ namespace AzFramework
return *this;
}
void OctreeNode::Insert(OctreeScene& octreeScene, VisibilityEntry* entry)
{
AZ_Assert(entry->m_internalNode == nullptr, "Double-insertion: Insert invoked for an entry already bound to the OctreeScene");
@@ -98,7 +93,6 @@ namespace AzFramework
}
}
void OctreeNode::Update(OctreeScene& octreeScene, VisibilityEntry* entry)
{
AZ_Assert(entry->m_internalNode == this, "Update invoked for an entry bound to a different OctreeNode");
@@ -129,7 +123,6 @@ namespace AzFramework
}
}
void OctreeNode::Remove(OctreeScene& octreeScene, VisibilityEntry* entry)
{
AZ_Assert(entry->m_internalNode == this, "Remove invoked for an entry bound to a different OctreeNode");
@@ -152,25 +145,30 @@ namespace AzFramework
}
}
void OctreeNode::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const
{
EnumerateHelper(aabb, callback);
if (AZ::ShapeIntersection::Overlaps(aabb, m_bounds))
{
EnumerateHelper(aabb, callback);
}
}
void OctreeNode::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const
{
EnumerateHelper(sphere, callback);
if (AZ::ShapeIntersection::Overlaps(sphere, m_bounds))
{
EnumerateHelper(sphere, callback);
}
}
void OctreeNode::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const
{
EnumerateHelper(frustum, callback);
if (AZ::ShapeIntersection::Overlaps(frustum, m_bounds))
{
EnumerateHelper(frustum, callback);
}
}
void OctreeNode::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const
{
// Invoke the callback for the current node
@@ -190,25 +188,21 @@ namespace AzFramework
}
}
const AZStd::vector<VisibilityEntry*>& OctreeNode::GetEntries() const
{
return m_entries;
}
OctreeNode* OctreeNode::GetChildren() const
{
return m_children;
}
bool OctreeNode::IsLeaf() const
{
return m_children == nullptr;
}
void OctreeNode::TryMerge(OctreeScene& octreeScene)
{
if (IsLeaf())
@@ -236,7 +230,6 @@ namespace AzFramework
}
}
template <typename T>
void OctreeNode::EnumerateHelper(const T& boundingVolume, const IVisibilityScene::EnumerateCallback& callback) const
{
@@ -262,7 +255,6 @@ namespace AzFramework
}
}
void OctreeNode::Split(OctreeScene& octreeScene)
{
AZ_Assert(m_children == nullptr, "Split invoked on an octreeScene node that has already been split");
@@ -312,7 +304,6 @@ namespace AzFramework
}
}
void OctreeNode::Merge(OctreeScene& octreeScene)
{
AZ_Assert(m_children != nullptr, "Merge invoked on an octreeScene node that does not have children");
@@ -371,7 +362,6 @@ namespace AzFramework
}
}
void OctreeScene::RemoveEntry(VisibilityEntry& entry)
{
AZStd::lock_guard<AZStd::shared_mutex> lock(m_sharedMutex);
@@ -382,35 +372,30 @@ namespace AzFramework
}
}
void OctreeScene::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.Enumerate(aabb, callback);
}
void OctreeScene::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.Enumerate(sphere, callback);
}
void OctreeScene::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.Enumerate(frustum, callback);
}
void OctreeScene::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.EnumerateNoCull(callback);
}
uint32_t OctreeScene::GetEntryCount() const
{
return m_entryCount;
@@ -421,26 +406,22 @@ namespace AzFramework
return m_nodeCount;
}
uint32_t OctreeScene::GetFreeNodeCount() const
{
// Each entry represents GetChildNodeCount() nodes
return aznumeric_cast<uint32_t>(m_freeOctreeNodes.size() * GetChildNodeCount());
}
uint32_t OctreeScene::GetPageCount() const
{
return aznumeric_cast<uint32_t>(m_nodeCache.size());
}
uint32_t OctreeScene::GetChildNodeCount() const
{
return AzFramework::GetChildNodeCount();
}
void OctreeScene::DumpStats()
{
AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::EntryCount = %u", GetName().GetCStr(), GetEntryCount());
@@ -450,21 +431,18 @@ namespace AzFramework
AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::ChildNodeCount = %u", GetName().GetCStr(), GetChildNodeCount());
}
static inline uint32_t CreateNodeIndex(uint32_t page, uint32_t offset)
{
AZ_Assert(page <= 0xFFFF && offset <= 0xFFFF, "Out of range values passed to CreateNodeIndex");
return (page << 16) | offset;
}
static inline void ExtractPageAndOffsetFromIndex(uint32_t index, uint32_t& page, uint32_t& offset)
{
offset = index & 0x0000FFFF;
page = index >> 16;
}
uint32_t OctreeScene::AllocateChildNodes()
{
const uint32_t childCount = GetChildNodeCount();
@@ -508,14 +486,12 @@ namespace AzFramework
return CreateNodeIndex(nextChildPage, nextChildOffset);
}
void OctreeScene::ReleaseChildNodes(uint32_t nodeIndex)
{
m_nodeCount -= GetChildNodeCount();
m_freeOctreeNodes.push(nodeIndex);
}
OctreeNode* OctreeScene::GetChildNodesAtIndex(uint32_t nodeIndex) const
{
uint32_t childPage;
@@ -524,7 +500,6 @@ namespace AzFramework
return &(*m_nodeCache[childPage])[childOffset];
}
void OctreeSystemComponent::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
@@ -534,19 +509,16 @@ namespace AzFramework
}
}
void OctreeSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("OctreeService"));
}
void OctreeSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("OctreeService"));
}
OctreeSystemComponent::OctreeSystemComponent()
{
AZ::Interface<IVisibilitySystem>::Register(this);
@@ -555,7 +527,6 @@ namespace AzFramework
m_defaultScene = aznew OctreeScene(AZ::Name("DefaultVisibilityScene"));
}
OctreeSystemComponent::~OctreeSystemComponent()
{
AZ_Assert(m_scenes.empty(), "All IVisibilityScenes must be destroyed before shutdown");
@@ -566,13 +537,11 @@ namespace AzFramework
AZ::Interface<IVisibilitySystem>::Unregister(this);
}
void OctreeSystemComponent::Activate()
{
;
}
void OctreeSystemComponent::Deactivate()
{
;
@@ -591,7 +560,6 @@ namespace AzFramework
return newScene;
}
void OctreeSystemComponent::DestroyVisibilityScene(IVisibilityScene* visScene)
{
for (auto iter = m_scenes.begin(); iter != m_scenes.end(); ++iter)
@@ -606,7 +574,6 @@ namespace AzFramework
AZ_Assert(false, "visScene[\"%s\"] not found in the OctreeSystemComponent", visScene->GetName().GetCStr());
}
IVisibilityScene* OctreeSystemComponent::FindVisibilityScene(const AZ::Name& sceneName)
{
for (OctreeScene* scene : m_scenes)
@@ -619,7 +586,6 @@ namespace AzFramework
return nullptr;
}
void OctreeSystemComponent::DumpStats([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
{
for (OctreeScene* scene : m_scenes)
@@ -228,6 +228,7 @@ set(FILES
Physics/Configuration/SimulatedBodyConfiguration.cpp
Physics/Configuration/SystemConfiguration.h
Physics/Configuration/SystemConfiguration.cpp
Physics/HeightfieldProviderBus.h
Physics/SimulatedBodies/RigidBody.h
Physics/SimulatedBodies/RigidBody.cpp
Physics/SimulatedBodies/StaticRigidBody.h
@@ -299,6 +300,8 @@ set(FILES
Spawnable/SpawnableMonitor.cpp
Spawnable/SpawnableSystemComponent.h
Spawnable/SpawnableSystemComponent.cpp
SurfaceData/SurfaceData.h
SurfaceData/SurfaceData.cpp
Terrain/TerrainDataRequestBus.h
Terrain/TerrainDataRequestBus.cpp
Thermal/ThermalInfo.h
+2 -1
View File
@@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
NAMESPACE AZ
FILES_CMAKE
Tests/framework_shared_tests_files.cmake
AzFramework/Physics/physics_mock_files.cmake
INCLUDE_DIRECTORIES
PUBLIC
Tests
@@ -53,7 +54,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest
AZ::AzTestShared
)
if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_add_target(
@@ -308,7 +308,7 @@ namespace AzFramework
event.data.data32[2] = 0;
event.data.data32[3] = 1;
event.data.data32[4] = 0;
xcb_void_cookie_t xcbCheckResult = xcb_send_event(
[[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event(
m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
(const char*)&event);
AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set _NET_WM_STATE_FULLSCREEN");
@@ -333,7 +333,7 @@ namespace AzFramework
event.data.data32[2] = 0;
event.data.data32[3] = 0;
event.data.data32[4] = 0;
xcb_void_cookie_t xcbCheckResult = xcb_send_event(
[[maybe_unused]] xcb_void_cookie_t xcbCheckResult = xcb_send_event(
m_xcbConnection, 1, m_xcbRootScreen->root, XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
(const char*)&event);
AZ_Assert(
@@ -7,17 +7,35 @@
*/
#include <AzFramework/Application/Application.h>
#include <sys/resource.h>
#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
#include <AzFramework/XcbApplication.h>
#endif
constexpr rlim_t g_minimumOpenFileHandles = 65536L;
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
Application::Implementation* Application::Implementation::Create()
{
// The default open file limit for processes may not be enough for O3DE applications.
// We will need to increase to the recommended value if the current open file limit
// is not sufficient.
rlimit currentLimit;
int get_limit_result = getrlimit(RLIMIT_NOFILE, &currentLimit);
AZ_Warning("Application", get_limit_result == 0, "Unable to read current ulimit open file limits");
if ((get_limit_result == 0) && (currentLimit.rlim_cur < g_minimumOpenFileHandles || currentLimit.rlim_max < g_minimumOpenFileHandles))
{
rlimit newLimit;
newLimit.rlim_cur = g_minimumOpenFileHandles; // Soft Limit
newLimit.rlim_max = g_minimumOpenFileHandles; // Hard Limit
[[maybe_unused]] int set_limit_result = setrlimit(RLIMIT_NOFILE, &newLimit);
AZ_Assert(set_limit_result == 0, "Unable to update open file limits");
}
#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
return aznew XcbApplication();
#elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND
@@ -91,31 +91,42 @@ namespace AzFramework
return processId == 0;
}
/*! Executes a command in the child process after the fork operation has been executed.
* This function will never return. If the execvp command fails this will call _exit with
* the errno value as the return value since continuing execution after a execvp command
* is invalid (it will be running the parent's code and in its address space and will
* cause many issues).
/*! Executes a command in the child process after the fork operation
* has been executed. This function will never return. If the execvpe
* command fails this will call _exit since continuing execution after
* a execvpe command is invalid (it will be running the parent's code
* and in its address space and will cause many issues).
*
* This function runs after a `fork()` call. `fork()` creates a copy of
* the current process, including the current state of the process's
* memory, at the time the call is made. However, it only creates a
* copy of the one thread that called `fork()`. This means that if any
* mutexes are locked by other threads at the time of `fork()`, those
* mutexes will remain locked in the child process, with no way to
* unlock them. So this function needs to ensure that it does as little
* work as possible.
*
* \param commandAndArgs - Array of strings that has the command to execute in index 0 with any args for the command following. Last element must be a null pointer.
* \param envionrmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param environmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param processLaunchInfo - struct containing information about luanching the command
* \param startupInfo - struct containing information needed to startup the command
* \param errorPipe - a pipe file descriptor used to communicate a failed execvpe call's error code to the parent process
*/
void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo)
[[noreturn]] static void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo, const AZStd::array<int, 2>& errorPipe)
{
close(errorPipe[0]);
if (!processLaunchInfo.m_workingDirectory.empty())
{
int res = chdir(processLaunchInfo.m_workingDirectory.c_str());
if (res != 0)
{
std::cerr << strerror(errno) << std::endl;
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to change the launched process' directory to '%s'.", processLaunchInfo.m_workingDirectory.c_str());
write(errorPipe[1], &errno, sizeof(int));
// We *have* to _exit as we are the child process and simply
// returning at this point would mean we would start running
// the code from our parent process and that will just wreck
// havoc.
_exit(errno);
_exit(0);
}
}
@@ -135,15 +146,17 @@ namespace AzFramework
startupInfo.SetupHandlesForChildProcess();
execve(commandAndArgs[0], commandAndArgs, environmentVariables);
execvpe(commandAndArgs[0], commandAndArgs, environmentVariables);
const int errval = errno;
// If we get here then execve failed to run the requested program and
// If we get here then execvpe failed to run the requested program and
// we have an error. In this case we need to exit the child process
// to stop it from continuing to run as a clone of the parent
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process %s : errno = %s ", commandAndArgs[0], strerror(errno));
std::cerr << strerror(errno) << std::endl;
// to stop it from continuing to run as a clone of the parent.
// Communicate the error code back to the parent via a pipe for the
// parent to read.
write(errorPipe[1], &errval, sizeof(errval));
_exit(errno);
_exit(0);
}
}
@@ -212,9 +225,8 @@ namespace AzFramework
AZStd::string outputString;
bool inQuotes = false;
for (size_t pos = 0; pos < processLaunchInfo.m_commandlineParameters.size(); ++pos)
for (const char currentChar : processLaunchInfo.m_commandlineParameters)
{
char currentChar = processLaunchInfo.m_commandlineParameters[pos];
if (currentChar == '"')
{
inQuotes = !inQuotes;
@@ -231,7 +243,7 @@ namespace AzFramework
outputString.push_back(currentChar);
}
}
if (!outputString.empty())
{
commandTokens.push_back(outputString);
@@ -249,10 +261,10 @@ namespace AzFramework
return false;
}
// Because of the way execve is defined we need to copy the strings from
// Because of the way execvpe is defined we need to copy the strings from
// AZ::string (using c_str() returns a const char*) into a non-const char*
// Need to add one more as exec requires the array's last element to be a null pointer
// Need to add one more as execvpe requires the array's last element to be a null pointer
char** commandAndArgs = new char*[commandTokens.size() + 1];
for (int i = 0; i < commandTokens.size(); ++i)
{
@@ -275,7 +287,7 @@ namespace AzFramework
azstrcat(environmentVariable.get(), envVarString.size() + 1, envVarString.c_str());
environmentVariablesVector.emplace_back(environmentVariable.get());
}
// Adding one more as exec expects the array to have a nullptr as the last element
// Adding one more as execvpe expects the array to have a nullptr as the last element
environmentVariablesVector.emplace_back(nullptr);
environmentVariables = environmentVariablesVector.data();
}
@@ -288,15 +300,50 @@ namespace AzFramework
AZ_Assert(environmentVariables, "Environment variables for current process not available\n");
}
// Set up a pipe to communicate the error code from the subprocess's execvpe call
AZStd::array<int, 2> childErrorPipeFds{};
pipe(childErrorPipeFds.data());
// This configures the write end of the pipe to close on calls to `exec`
fcntl(childErrorPipeFds[1], F_SETFD, fcntl(childErrorPipeFds[1], F_GETFD) | FD_CLOEXEC);
pid_t child_pid = fork();
if (IsIdChildProcess(child_pid))
{
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo);
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo, childErrorPipeFds);
}
processData.m_childProcessId = child_pid;
// Close these handles as they are only to be used by the child process
processData.m_startupInfo.CloseAllHandles();
close(childErrorPipeFds[1]);
{
int errorCodeFromChild = 0;
int count = 0;
// Read from the error pipe.
// * If the child's call to execvpe succeeded, then the pipe will
// be closed due to setting FD_CLOEXEC on the write end of the
// pipe. `read()` will return 0.
// * If the child's call to execvpe failed, the child will have
// written the error code to the pipe. `read()` will return >0, and
// the data to be read is the error code from execvpe.
while ((count = read(childErrorPipeFds[0], &errorCodeFromChild, sizeof(errorCodeFromChild))) == -1)
{
if (errno != EAGAIN && errno != EINTR)
{
break;
}
}
if (count)
{
AZ_TracePrintf("Process Watcher", "ProcessLauncher::LaunchProcess: Unable to launch process %s : errno = %s\n", commandAndArgs[0], strerror(errorCodeFromChild));
processData.m_childProcessIsDone = true;
child_pid = -1;
}
}
close(childErrorPipeFds[0]);
processData.m_childProcessId = child_pid;
for (int i = 0; i < commandTokens.size(); i++)
{
@@ -53,7 +53,7 @@ namespace AzNetworking
m_timeoutItemMap.erase(timeoutId);
}
void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts)
void TimeoutQueue::UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts)
{
int32_t numTimeouts = 0;
if (maxTimeouts < 0)
@@ -103,7 +103,7 @@ namespace AzNetworking
// By this point, the item is definitely timed out
// Invoke the timeout function to see how to proceed
const TimeoutResult result = timeoutHandler.HandleTimeout(mapItem);
const TimeoutResult result = timeoutHandler(mapItem);
if (result == TimeoutResult::Refresh)
{
@@ -122,4 +122,10 @@ namespace AzNetworking
m_timeoutItemMap.erase(itemTimeoutId);
}
}
void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts)
{
TimeoutHandler handler([&timeoutHandler](TimeoutQueue::TimeoutItem& item) { return timeoutHandler.HandleTimeout(item); });
UpdateTimeouts(handler, maxTimeouts);
}
}
@@ -64,6 +64,12 @@ namespace AzNetworking
//! @param timeoutId the identifier of the item to remove
void RemoveItem(TimeoutId timeoutId);
//! Updates timeouts for all items, invokes the provided timeout functor if required.
//! @param timeoutHandler lambda to invoke for all timeouts
//! @param maxTimeouts the maximum number of timeouts to process before breaking iteration
using TimeoutHandler = AZStd::function<TimeoutResult(TimeoutQueue::TimeoutItem&)>;
void UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts = -1);
//! Updates timeouts for all items, invokes timeout handlers if required.
//! @param timeoutHandler listener instance to call back on for timeouts
//! @param maxTimeouts the maximum number of timeouts to process before breaking iteration
@@ -0,0 +1,229 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Component/Entity.h>
#include <AzQtComponents/Components/ToastNotification.h>
#include <AzQtComponents/Components/ui_ToastNotification.h>
#include <QCursor>
#include <QIcon>
#include <QToolButton>
#include <QPropertyAnimation>
#include <QPainter>
namespace AzQtComponents
{
ToastNotification::ToastNotification(QWidget* parent, const ToastConfiguration& toastConfiguration)
: QDialog(parent, Qt::FramelessWindowHint)
, m_closeOnClick(true)
, m_ui(new Ui::ToastNotification())
, m_fadeAnimation(nullptr)
{
setProperty("HasNoWindowDecorations", true);
setAttribute(Qt::WA_ShowWithoutActivating);
setAttribute(Qt::WA_DeleteOnClose);
m_borderRadius = toastConfiguration.m_borderRadius;
if (m_borderRadius > 0)
{
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
setAttribute(Qt::WA_TranslucentBackground);
}
m_ui->setupUi(this);
QIcon toastIcon;
switch (toastConfiguration.m_toastType)
{
case ToastType::Error:
toastIcon = QIcon(":/stylesheet/img/logging/error.svg");
break;
case ToastType::Warning:
toastIcon = QIcon(":/stylesheet/img/logging/warning-yellow.svg");
break;
case ToastType::Information:
toastIcon = QIcon(":/stylesheet/img/logging/information.svg");
break;
case ToastType::Custom:
toastIcon = QIcon(toastConfiguration.m_customIconImage);
default:
break;
}
m_ui->iconLabel->setPixmap(toastIcon.pixmap(64, 64));
m_ui->titleLabel->setText(toastConfiguration.m_title);
m_ui->mainLabel->setText(toastConfiguration.m_description);
// hide the optional description if none is provided so the title is centered vertically
if (toastConfiguration.m_description.isEmpty())
{
m_ui->mainLabel->setVisible(false);
m_ui->verticalLayout->removeWidget(m_ui->mainLabel);
}
m_lifeSpan.setInterval(aznumeric_cast<int>(toastConfiguration.m_duration.count()));
m_closeOnClick = toastConfiguration.m_closeOnClick;
m_ui->closeButton->setVisible(m_closeOnClick);
QObject::connect(m_ui->closeButton, &QToolButton::clicked, this, &ToastNotification::accept);
m_fadeDuration = toastConfiguration.m_fadeDuration;
QObject::connect(&m_lifeSpan, &QTimer::timeout, this, &ToastNotification::FadeOut);
}
ToastNotification::~ToastNotification()
{
}
void ToastNotification::paintEvent(QPaintEvent* event)
{
if (m_borderRadius > 0)
{
QPainter p(this);
p.setPen(Qt::transparent);
QColor painterColor;
painterColor.setRgbF(0, 0, 0, 255);
p.setBrush(painterColor);
p.setRenderHint(QPainter::Antialiasing);
p.drawRoundedRect(rect(), m_borderRadius, m_borderRadius);
}
else
{
QDialog::paintEvent(event);
}
}
void ToastNotification::ShowToastAtCursor()
{
QPoint globalCursorPos = QCursor::pos();
// Left/middle align it relative to the cursor.
QPointF anchorPoint(0, 0.5);
// Magic offset to try to get it to not hide under the cursor.
// No way to get this programatically from what I can tell.
globalCursorPos.setX(globalCursorPos.x() + 16);
ShowToastAtPoint(globalCursorPos, anchorPoint);
}
void ToastNotification::ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint)
{
show();
updateGeometry();
UpdatePosition(screenPosition, anchorPoint);
}
void ToastNotification::UpdatePosition(const QPoint& screenPosition, const QPointF& anchorPoint)
{
QRect dialogGeometry = geometry();
QPoint finalPosition;
finalPosition.setX(aznumeric_cast<int>(screenPosition.x() - dialogGeometry.width() * anchorPoint.x()));
finalPosition.setY(aznumeric_cast<int>(screenPosition.y() - dialogGeometry.height() * anchorPoint.y()));
move(finalPosition);
}
void ToastNotification::showEvent(QShowEvent* showEvent)
{
QDialog::showEvent(showEvent);
if (m_fadeDuration.count() > 0)
{
m_fadeAnimation = new QPropertyAnimation(this, "windowOpacity", this);
m_fadeAnimation->setKeyValueAt(0, 0);
m_fadeAnimation->setKeyValueAt(1, 1);
m_fadeAnimation->setDuration(static_cast<int>(m_fadeDuration.count()));
m_fadeAnimation->start();
QObject::connect(m_fadeAnimation, &QPropertyAnimation::finished, this, &ToastNotification::StartTimer);
}
else
{
StartTimer();
}
}
void ToastNotification::hideEvent(QHideEvent* hideEvent)
{
QDialog::hideEvent(hideEvent);
m_lifeSpan.stop();
if (m_fadeAnimation)
{
m_fadeAnimation->stop();
delete m_fadeAnimation;
}
emit ToastNotificationHidden();
}
void ToastNotification::mousePressEvent(QMouseEvent*)
{
if (m_closeOnClick)
{
emit ToastNotificationInteraction();
accept();
}
}
bool ToastNotification::eventFilter(QObject*, QEvent* event)
{
if (event->type() == QEvent::MouseButtonPress)
{
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent && mouseEvent->button() == Qt::MouseButton::LeftButton)
{
accept();
}
}
return false;
}
void ToastNotification::StartTimer()
{
delete m_fadeAnimation;
m_fadeAnimation = nullptr;
if (m_lifeSpan.interval() != 0)
{
m_lifeSpan.start();
}
}
void ToastNotification::FadeOut()
{
if (m_fadeDuration.count() > 0)
{
m_fadeAnimation = new QPropertyAnimation(this, "windowOpacity", this);
m_fadeAnimation->setKeyValueAt(0, windowOpacity());
m_fadeAnimation->setKeyValueAt(1, 0);
m_fadeAnimation->setDuration(static_cast<int>(m_fadeDuration.count()));
m_fadeAnimation->start();
QObject::connect(m_fadeAnimation, &QPropertyAnimation::finished, this, &ToastNotification::accept);
}
else
{
accept();
}
}
#include "Components/moc_ToastNotification.cpp"
}
@@ -0,0 +1,77 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzQtComponents/AzQtComponentsAPI.h>
#include <AzQtComponents/Components/ToastNotificationConfiguration.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <QEvent>
#include <QDialog>
#include <QMouseEvent>
#include <QTimer>
#endif
namespace Ui
{
class ToastNotification;
}
QT_FORWARD_DECLARE_CLASS(QPropertyAnimation)
namespace AzQtComponents
{
class AZ_QT_COMPONENTS_API ToastNotification
: public QDialog
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(ToastNotification, AZ::SystemAllocator, 0);
ToastNotification(QWidget* parent, const ToastConfiguration& toastConfiguration);
virtual ~ToastNotification();
// Shows the toast notification relative to the current cursor.
void ShowToastAtCursor();
// Aligns the toast notification so that the specified anchor point on the notification lies on the specified screen position.
// i.e. anchor point of 0,0 will align the top left position of the dialog with the screen position
// anchor point of 1,1 will align the bottom right position of the dialog with the screen position
void ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint);
void UpdatePosition(const QPoint& screenPosition, const QPointF& anchorPoint);
// QDialog
void showEvent(QShowEvent* showEvent) override;
void hideEvent(QHideEvent* hideEvent) override;
void mousePressEvent(QMouseEvent* mouseEvent) override;
bool eventFilter(QObject* object, QEvent* event) override;
void paintEvent(QPaintEvent* event) override;
public slots:
void StartTimer();
void FadeOut();
signals:
void ToastNotificationHidden();
void ToastNotificationInteraction();
private:
QPropertyAnimation* m_fadeAnimation;
bool m_closeOnClick;
QTimer m_lifeSpan;
uint32_t m_borderRadius = 0;
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
AZStd::chrono::milliseconds m_fadeDuration;
AZStd::unique_ptr<Ui::ToastNotification> m_ui;
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
};
} // namespace AzQtComponents
@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ToastNotification</class>
<widget class="QDialog" name="ToastNotification">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>225</width>
<height>48</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>225</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="icon_frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgba(255, 255, 255, 20);</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="iconLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgba(255, 255, 255, 0);</string>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="resources.qrc">:/stylesheet/img/logging/information.svg</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="text_frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QFrame" name="titleFrame">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="titleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Invalid Connection</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="closeButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/stylesheet/img/close_x.svg</normaloff>:/stylesheet/img/close_x.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="mainLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Types are not a match.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,18 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzQtComponents/Components/ToastNotificationConfiguration.h>
namespace AzQtComponents
{
ToastConfiguration::ToastConfiguration(ToastType toastType, const QString& title, const QString& description)
: m_toastType(toastType)
, m_title(title)
, m_description(description)
{
}
} // namespace AzQtComponents
@@ -0,0 +1,47 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzQtComponents/AzQtComponentsAPI.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/chrono/chrono.h>
#include <QString>
#endif
namespace AzQtComponents
{
enum class ToastType
{
Information,
Warning,
Error,
Custom
};
class AZ_QT_COMPONENTS_API ToastConfiguration
{
public:
AZ_CLASS_ALLOCATOR(ToastConfiguration, AZ::SystemAllocator, 0);
ToastConfiguration(ToastType toastType, const QString& title, const QString& description);
bool m_closeOnClick = true;
ToastType m_toastType = ToastType::Information;
QString m_title;
QString m_description;
QString m_customIconImage;
uint32_t m_borderRadius = 0;
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds(5000);
AZStd::chrono::milliseconds m_fadeDuration = AZStd::chrono::milliseconds(250);
AZ_POP_DISABLE_DLL_EXPORT_MEMBER_WARNING
};
} // namespace AzQtComponents
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.38947 2H2.97474C1.61046 2 0.5 3.09717 0.5 4.44876V13.0353C0.5 14.4028 1.61046 15.5 2.97474 15.5H11.5253C12.8895 15.5 14 14.4028 14 13.0353V9.61053C13.5767 9.88652 13.1131 10.1058 12.6199 10.2576V13.0353H12.5881C12.5881 13.6396 12.0964 14.1007 11.5094 14.1007H2.97474C2.37192 14.1007 1.896 13.6237 1.896 13.0353V4.44876C1.896 3.86042 2.38778 3.38339 2.97474 3.38339H5.74142C5.89324 2.88897 6.11287 2.42422 6.38947 2Z" fill="white"/>
<path d="M11 0.5C8.51446 0.5 6.5 2.51471 6.5 5C6.5 7.48529 8.51446 9.5 11 9.5C13.485 9.5 15.5 7.48529 15.5 5C15.5 2.51471 13.485 0.5 11 0.5ZM13.8633 6.39526C13.9155 6.43923 13.8975 6.54774 13.8221 6.63723L13.1024 7.49454C13.0276 7.58429 12.9237 7.62106 12.8715 7.57708L11.0003 6.00697L9.12903 7.57683C9.07683 7.62106 8.97346 7.58403 8.89811 7.49454L8.17837 6.63723C8.10354 6.54749 8.08503 6.43897 8.13723 6.39526L9.80017 4.99974L8.13723 3.60449C8.08503 3.56026 8.10303 3.452 8.17837 3.36251L8.8976 2.5052C8.97294 2.4152 9.07631 2.37869 9.12903 2.42266L11.0003 3.99277L12.8715 2.42266C12.9242 2.37869 13.0276 2.41546 13.1029 2.5052L13.8221 3.36251C13.8975 3.452 13.9155 3.56051 13.8633 3.60449L12.2003 5L13.8633 6.39526Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5881 13.0353C12.5881 13.6396 12.0964 14.1007 11.5094 14.1007H2.97474C2.37192 14.1007 1.896 13.6237 1.896 13.0353V4.44876C1.896 3.86042 2.38778 3.38339 2.97474 3.38339H7.33725L8.71739 2H2.97474C1.61046 2 0.5 3.09717 0.5 4.44876V13.0353C0.5 14.4028 1.61046 15.5 2.97474 15.5H11.5253C12.8895 15.5 14 14.4028 14 13.0353V7.26325L12.6199 8.64664V13.0353H12.5881Z" fill="white"/>
<path d="M15.1805 2.87326L13.1392 0.850975C12.9217 0.633705 12.6205 0.5 12.3193 0.5C12.0014 0.5 11.717 0.616992 11.4995 0.834262L3.83621 8.41708C3.63543 8.61764 3.5183 8.88505 3.50157 9.16917L3.50157 11.2632C3.48484 11.5975 3.60196 11.9318 3.83621 12.1657C4.05373 12.383 4.3549 12.5 4.65608 12.5C4.67281 12.5 4.70628 12.5 4.72301 12.5H6.69739C6.98183 12.4833 7.24954 12.3663 7.45033 12.1657L15.1638 4.52786C15.3813 4.31059 15.4984 4.00975 15.4984 3.70891C15.5152 3.39137 15.398 3.09053 15.1805 2.87326ZM10.3784 4.02646L12.0014 5.64763L8.23673 9.39136L6.61373 7.77019L10.3784 4.02646ZM6.69739 10.929L4.97399 11.0292L5.07438 9.3078L5.55961 8.82312L7.18261 10.4443L6.69739 10.929ZM13.0221 4.59471L11.4158 2.99025L12.3193 2.08774L13.9423 3.70891L13.0221 4.59471Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -6,6 +6,8 @@
<file alias="layer.svg">Entity/layer.svg</file>
<file alias="prefab.svg">Entity/prefab.svg</file>
<file alias="prefab_edit.svg">Entity/prefab_edit.svg</file>
<file alias="prefab_edit_open.svg">Entity/prefab_edit_open.svg</file>
<file alias="prefab_edit_close.svg">Entity/prefab_edit_close.svg</file>
</qresource>
<qresource prefix="/Level">
<file alias="level.svg">Level/level.svg</file>
@@ -28,4 +28,21 @@ namespace AzQtComponents
return scaledPixmap;
}
QPixmap CropPixmapForScreenDpi(
QPixmap pixmap, QScreen* screen, QRect rect)
{
qreal screenDpiFactor = QHighDpiScaling::factor(screen);
pixmap.setDevicePixelRatio(screenDpiFactor);
QRect cropRect(
aznumeric_cast<int>(aznumeric_cast<qreal>(rect.left()) * screenDpiFactor),
aznumeric_cast<int>(aznumeric_cast<qreal>(rect.top()) * screenDpiFactor),
aznumeric_cast<int>(aznumeric_cast<qreal>(rect.width()) * screenDpiFactor),
aznumeric_cast<int>(aznumeric_cast<qreal>(rect.height()) * screenDpiFactor)
);
QPixmap croppedPixmap = pixmap.copy(cropRect);
return croppedPixmap;
}
}
@@ -11,9 +11,11 @@
#include <AzQtComponents/AzQtComponentsAPI.h>
#include <QPixmap>
#include <QRect>
#include <QScreen>
namespace AzQtComponents
{
AZ_QT_COMPONENTS_API QPixmap ScalePixmapForScreenDpi(QPixmap pixmap, QScreen* screen, QSize size, Qt::AspectRatioMode aspectRatioMode, Qt::TransformationMode transformationMode);
AZ_QT_COMPONENTS_API QPixmap CropPixmapForScreenDpi(QPixmap pixmap, QScreen* screen, QRect rect);
}; // namespace AzQtComponents
@@ -49,6 +49,11 @@ set(FILES
Components/Titlebar.h
Components/TitleBarOverdrawHandler.cpp
Components/TitleBarOverdrawHandler.h
Components/ToastNotification.cpp
Components/ToastNotification.h
Components/ToastNotificationConfiguration.h
Components/ToastNotificationConfiguration.cpp
Components/ToastNotification.ui
Components/ToolButtonComboBox.cpp
Components/ToolButtonComboBox.h
Components/ToolButtonLineEdit.cpp
@@ -7,6 +7,7 @@
*/
#include <dlfcn.h>
#include <iostream>
#include <AzCore/IO/Path/Path.h>
#include <AzTest/Platform.h>
#include <sys/types.h>
@@ -20,11 +21,16 @@ public:
explicit ModuleHandle(const std::string& lib)
: m_libHandle(nullptr)
{
std::string libext = lib;
if (!AZ::Test::EndsWith(libext, ".dylib"))
AZ::IO::FixedMaxPath libext = AZStd::string_view{ lib.c_str(), lib.size() };
if (!libext.Stem().Native().starts_with(AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX))
{
libext += ".dylib";
libext = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX + libext.Native();
}
if (libext.Extension() != AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION)
{
libext.Native() += AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
}
m_libHandle = dlopen(libext.c_str(), RTLD_NOW);
const char* error = dlerror();
if (error)
@@ -36,6 +36,10 @@ namespace AzToolsFramework
//! the entity type.
virtual AZStd::vector<AZ::Uuid> FindComponentTypeIdsByEntityType(const AZStd::vector<AZStd::string>& componentTypeNames, EntityType entityType) = 0;
//! Return a list of type ids for components that match the required services filter,
//! and don't conflict with any of the incompatible services filter
virtual AZStd::vector<AZ::Uuid> FindComponentTypeIdsByService(const AZStd::vector<AZ::ComponentServiceType>& serviceFilter, const AZStd::vector<AZ::ComponentServiceType>& incompatibleServiceFilter) = 0;
//! Finds the component names from their type ids
virtual AZStd::vector<AZStd::string> FindComponentTypeNames(const AZ::ComponentTypeList& componentTypeIds) = 0;
@@ -15,6 +15,7 @@
#include <AzToolsFramework/ToolsComponents/EditorDisabledCompositionBus.h>
#include <AzToolsFramework/ToolsComponents/EditorPendingCompositionBus.h>
#include <AzToolsFramework/Entity/EditorEntityActionComponent.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/UI/PropertyEditor/InstanceDataHierarchy.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
@@ -75,6 +76,7 @@ namespace AzToolsFramework
serializeContext->Class<EditorComponentAPIComponent, AZ::Component>();
serializeContext->RegisterGenericType<AZStd::vector<AZ::EntityComponentIdPair>>();
serializeContext->RegisterGenericType<AZStd::vector<AZ::ComponentServiceType>>();
}
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
@@ -99,6 +101,7 @@ namespace AzToolsFramework
->Attribute(AZ::Script::Attributes::Module, "editor")
->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::All)
->Event("FindComponentTypeIdsByEntityType", &EditorComponentAPIRequests::FindComponentTypeIdsByEntityType)
->Event("FindComponentTypeIdsByService", &EditorComponentAPIRequests::FindComponentTypeIdsByService)
->Event("FindComponentTypeNames", &EditorComponentAPIRequests::FindComponentTypeNames)
->Event("BuildComponentTypeNameListByEntityType", &EditorComponentAPIRequests::BuildComponentTypeNameListByEntityType)
->Event("AddComponentsOfType", &EditorComponentAPIRequests::AddComponentsOfType)
@@ -216,6 +219,33 @@ namespace AzToolsFramework
return foundTypeIds;
}
AZStd::vector<AZ::Uuid> EditorComponentAPIComponent::FindComponentTypeIdsByService(const AZStd::vector<AZ::ComponentServiceType>& serviceFilter, const AZStd::vector<AZ::ComponentServiceType>& incompatibleServiceFilter)
{
AZStd::vector<AZ::Uuid> foundTypeIds;
m_serializeContext->EnumerateDerived<AZ::Component>(
[&foundTypeIds, serviceFilter, incompatibleServiceFilter](const AZ::SerializeContext::ClassData* componentClass, const AZ::Uuid& knownType) -> bool
{
AZ_UNUSED(knownType);
if (componentClass->m_editData)
{
// If none of the required services are offered by this component, or the component
// can not be added by the user, skip to the next component
if (!OffersRequiredServices(componentClass, serviceFilter, incompatibleServiceFilter))
{
return true;
}
foundTypeIds.push_back(componentClass->m_typeId);
}
return true;
});
return foundTypeIds;
}
AZStd::vector<AZStd::string> EditorComponentAPIComponent::FindComponentTypeNames(const AZ::ComponentTypeList& componentTypeIds)
{
AZStd::vector<AZStd::string> foundTypeNames;
@@ -35,6 +35,7 @@ namespace AzToolsFramework
// EditorComponentAPIBus ...
AZStd::vector<AZ::Uuid> FindComponentTypeIdsByEntityType(const AZStd::vector<AZStd::string>& componentTypeNames, EditorComponentAPIRequests::EntityType entityType) override;
AZStd::vector<AZ::Uuid> FindComponentTypeIdsByService(const AZStd::vector<AZ::ComponentServiceType>& serviceFilter, const AZStd::vector<AZ::ComponentServiceType>& incompatibleServiceFilter) override;
AZStd::vector<AZStd::string> FindComponentTypeNames(const AZ::ComponentTypeList& componentTypeIds) override;
AZStd::vector<AZStd::string> BuildComponentTypeNameListByEntityType(EditorComponentAPIRequests::EntityType entityType) override;
@@ -271,6 +271,68 @@ namespace AzToolsFramework
return editorComponentBaseComponent;
}
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter,
const AZStd::vector<AZ::ComponentServiceType>& incompatibleServiceFilter
)
{
AZ_Assert(componentClass, "Component class must not be null");
if (!componentClass)
{
return false;
}
AZ::ComponentDescriptor* componentDescriptor = nullptr;
AZ::ComponentDescriptorBus::EventResult(
componentDescriptor, componentClass->m_typeId, &AZ::ComponentDescriptor::GetDescriptor);
if (!componentDescriptor)
{
return false;
}
// If no services are provided, this function returns true
if (serviceFilter.empty())
{
return true;
}
AZ::ComponentDescriptor::DependencyArrayType providedServices;
componentDescriptor->GetProvidedServices(providedServices, nullptr);
//reject this component if it does not offer any of the required services
if (AZStd::find_first_of(
providedServices.begin(),
providedServices.end(),
serviceFilter.begin(),
serviceFilter.end()) == providedServices.end())
{
return false;
}
//reject this component if it does offer any of the incompatible services
if (AZStd::find_first_of(
providedServices.begin(),
providedServices.end(),
incompatibleServiceFilter.begin(),
incompatibleServiceFilter.end()) != providedServices.end())
{
return false;
}
return true;
}
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter
)
{
const AZStd::vector<AZ::ComponentServiceType> incompatibleServices;
return OffersRequiredServices(componentClass, serviceFilter, incompatibleServices);
}
bool ShouldInspectorShowComponent(const AZ::Component* component)
{
if (!component)
@@ -105,6 +105,16 @@ namespace AzToolsFramework
AZ::ComponentDescriptor* GetComponentDescriptor(const AZ::Component* component);
Components::EditorComponentDescriptor* GetEditorComponentDescriptor(const AZ::Component* component);
Components::EditorComponentBase* GetEditorComponent(AZ::Component* component);
// Returns true if the given component provides at least one of the services specified or no services are provided
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter,
const AZStd::vector<AZ::ComponentServiceType>& incompatibleServiceFilter
);
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter
);
/// Return true if the editor should show this component to users,
/// false if the component should be hidden from users.
@@ -53,7 +53,7 @@ namespace AzToolsFramework
AZ_Assert(m_loaderInterface != nullptr,
"Couldn't get prefab loader interface, it's a requirement for PrefabEntityOwnership system to work");
m_rootInstance = AZStd::unique_ptr<Prefab::Instance>(m_prefabSystemComponent->CreatePrefab({}, {}, "NewLevel.prefab"));
m_rootInstance = AZStd::unique_ptr<Prefab::Instance>(m_prefabSystemComponent->CreatePrefab({}, {}, "newLevel.prefab"));
m_sliceOwnershipService.BusConnect(m_entityContextId);
m_sliceOwnershipService.m_shouldAssertForLegacySlicesUsage = m_shouldAssertForLegacySlicesUsage;
m_editorSliceOwnershipService.BusConnect();
@@ -244,6 +244,17 @@ namespace AzToolsFramework
const auto eventType = event->type();
if (eventType == QEvent::Type::MouseMove)
{
// clear override cursor when moving outside of the viewport
const auto* mouseEvent = static_cast<const QMouseEvent*>(event);
if (m_overrideCursor && !m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(mouseEvent->globalPos())))
{
qApp->restoreOverrideCursor();
m_overrideCursor = false;
}
}
// Only accept mouse & key release events that originate from an object that is not our target widget,
// as we don't want to erroneously intercept user input meant for another component.
if (object != m_sourceWidget && eventType != QEvent::Type::KeyRelease && eventType != QEvent::Type::MouseButtonRelease)
@@ -262,7 +273,7 @@ namespace AzToolsFramework
if (eventType == QEvent::FocusIn)
{
const auto globalCursorPosition = QCursor::pos();
if (m_sourceWidget->geometry().contains(globalCursorPosition))
if (m_sourceWidget->geometry().contains(m_sourceWidget->mapFromGlobal(globalCursorPosition)))
{
HandleMouseMoveEvent(globalCursorPosition);
}
@@ -452,4 +463,32 @@ namespace AzToolsFramework
}
}
}
static Qt::CursorShape QtCursorFromAzCursor(const ViewportInteraction::CursorStyleOverride cursorStyleOverride)
{
switch (cursorStyleOverride)
{
case ViewportInteraction::CursorStyleOverride::Forbidden:
return Qt::ForbiddenCursor;
default:
return Qt::ArrowCursor;
}
}
void QtEventToAzInputMapper::SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride)
{
ClearOverrideCursor();
qApp->setOverrideCursor(QtCursorFromAzCursor(cursorStyleOverride));
m_overrideCursor = true;
}
void QtEventToAzInputMapper::ClearOverrideCursor()
{
if (m_overrideCursor)
{
qApp->restoreOverrideCursor();
m_overrideCursor = false;
}
}
} // namespace AzToolsFramework
@@ -15,10 +15,11 @@
#include <AzFramework/Input/Channels/InputChannelDeltaWithSharedPosition2D.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedModifierKeyStates.h>
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedPosition2D.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <QEvent>
#include <QObject>
#include <QPoint>
@@ -55,6 +56,9 @@ namespace AzToolsFramework
//! like a dolly or rotation, where mouse movement is important but cursor location is not.
void SetCursorCaptureEnabled(bool enabled);
void SetOverrideCursor(ViewportInteraction::CursorStyleOverride cursorStyleOverride);
void ClearOverrideCursor();
// QObject overrides...
bool eventFilter(QObject* object, QEvent* event) override;
@@ -164,6 +168,8 @@ namespace AzToolsFramework
bool m_enabled = true;
// Flags whether or not the cursor is being constrained to the source widget (for invisible mouse movement).
bool m_capturingCursor = false;
// Flags whether the cursor has been overridden.
bool m_overrideCursor = false;
// Our viewport-specific AZ devices. We control their internal input channel states.
AZStd::unique_ptr<EditorQtMouseDevice> m_mouseDevice;
@@ -9,6 +9,8 @@
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Uuid.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
@@ -38,6 +40,13 @@ namespace AzToolsFramework
AZ::Edit::SliceFlags::DontGatherReference);
}
}
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty(
"EditorPrefabComponentTypeId", BehaviorConstant(AZ::Uuid(EditorPrefabComponent::EditorPrefabComponentTypeId)))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
}
}
void EditorPrefabComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
@@ -16,7 +16,9 @@ namespace AzToolsFramework
class EditorPrefabComponent : public AzToolsFramework::Components::EditorComponentBase
{
public:
AZ_COMPONENT(EditorPrefabComponent, "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}", EditorComponentBase);
static constexpr const char* const EditorPrefabComponentTypeId = "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}";
AZ_COMPONENT(EditorPrefabComponent, EditorPrefabComponentTypeId, EditorComponentBase);
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
@@ -16,6 +16,7 @@
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusNotificationBus.h>
#include <AzToolsFramework/Prefab/PrefabFocusUndo.h>
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
namespace AzToolsFramework::Prefab
{
@@ -28,7 +29,9 @@ namespace AzToolsFramework::Prefab
"Instance Entity Mapper Interface could not be found. "
"Check that it is being correctly initialized.");
EditorEntityInfoNotificationBus::Handler::BusConnect();
EditorEntityContextNotificationBus::Handler::BusConnect();
PrefabPublicNotificationBus::Handler::BusConnect();
AZ::Interface<PrefabFocusInterface>::Register(this);
AZ::Interface<PrefabFocusPublicInterface>::Register(this);
}
@@ -37,10 +40,12 @@ namespace AzToolsFramework::Prefab
{
AZ::Interface<PrefabFocusPublicInterface>::Unregister(this);
AZ::Interface<PrefabFocusInterface>::Unregister(this);
PrefabPublicNotificationBus::Handler::BusDisconnect();
EditorEntityContextNotificationBus::Handler::BusDisconnect();
EditorEntityInfoNotificationBus::Handler::BusDisconnect();
}
void PrefabFocusHandler::Initialize()
void PrefabFocusHandler::InitializeEditorInterfaces()
{
m_containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get();
AZ_Assert(
@@ -55,13 +60,6 @@ namespace AzToolsFramework::Prefab
"Prefab - PrefabFocusHandler - "
"Focus Mode Interface could not be found. "
"Check that it is being correctly initialized.");
m_instanceEntityMapperInterface = AZ::Interface<InstanceEntityMapperInterface>::Get();
AZ_Assert(
m_instanceEntityMapperInterface,
"Prefab - PrefabFocusHandler - "
"Instance Entity Mapper Interface could not be found. "
"Check that it is being correctly initialized.");
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnOwningPrefab(AZ::EntityId entityId)
@@ -82,7 +80,7 @@ namespace AzToolsFramework::Prefab
auto editUndo = aznew PrefabFocusUndo("Edit Prefab");
editUndo->Capture(entityId);
editUndo->SetParent(undoBatch.GetUndoBatch());
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, editUndo);
FocusOnPrefabInstanceOwningEntityId(entityId);
}
return AZ::Success();
@@ -90,16 +88,14 @@ namespace AzToolsFramework::Prefab
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPathIndex([[maybe_unused]] AzFramework::EntityContextId entityContextId, int index)
{
if (index < 0 || index >= m_instanceFocusVector.size())
if (index < 0 || index >= m_instanceFocusHierarchy.size())
{
return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex."));
}
InstanceOptionalReference focusedInstance = m_instanceFocusVector[index];
InstanceOptionalReference focusedInstance = m_instanceFocusHierarchy[index];
FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId());
return AZ::Success();
return FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId());
}
PrefabFocusOperationResult PrefabFocusHandler::FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId)
@@ -134,42 +130,38 @@ namespace AzToolsFramework::Prefab
return AZ::Failure(AZStd::string("Prefab Focus Handler: invalid instance to focus on."));
}
if (!m_isInitialized)
// Close all container entities in the old path.
CloseInstanceContainers(m_instanceFocusHierarchy);
m_focusedInstance = focusedInstance;
m_focusedTemplateId = focusedInstance->get().GetTemplateId();
AZ::EntityId containerEntityId;
if (focusedInstance->get().GetParentInstance() != AZStd::nullopt)
{
Initialize();
containerEntityId = focusedInstance->get().GetContainerEntityId();
}
if (!m_focusedInstance.has_value() || &m_focusedInstance->get() != &focusedInstance->get())
else
{
// Close all container entities in the old path
CloseInstanceContainers(m_instanceFocusVector);
m_focusedInstance = focusedInstance;
m_focusedTemplateId = focusedInstance->get().GetTemplateId();
AZ::EntityId containerEntityId;
if (focusedInstance->get().GetParentInstance() != AZStd::nullopt)
{
containerEntityId = focusedInstance->get().GetContainerEntityId();
}
else
{
containerEntityId = AZ::EntityId();
}
containerEntityId = AZ::EntityId();
}
// Focus on the descendants of the container entity
// Focus on the descendants of the container entity in the Editor, if the interface is initialized.
if (m_focusModeInterface)
{
m_focusModeInterface->SetFocusRoot(containerEntityId);
// Refresh path variables
RefreshInstanceFocusList();
// Open all container entities in the new path
OpenInstanceContainers(m_instanceFocusVector);
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
// Refresh path variables.
RefreshInstanceFocusList();
RefreshInstanceFocusPath();
// Open all container entities in the new path.
OpenInstanceContainers(m_instanceFocusHierarchy);
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
return AZ::Success();
}
@@ -213,6 +205,34 @@ namespace AzToolsFramework::Prefab
return instance.has_value() && (&instance->get() == &m_focusedInstance->get());
}
bool PrefabFocusHandler::IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const
{
if (!m_focusedInstance.has_value())
{
// PrefabFocusHandler has not been initialized yet.
return false;
}
if (!entityId.IsValid())
{
return false;
}
InstanceOptionalReference instance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
while (instance.has_value())
{
if (&instance->get() == &m_focusedInstance->get())
{
return true;
}
instance = instance->get().GetParentInstance();
}
return false;
}
const AZ::IO::Path& PrefabFocusHandler::GetPrefabFocusPath([[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
return m_instanceFocusPath;
@@ -220,49 +240,124 @@ namespace AzToolsFramework::Prefab
const int PrefabFocusHandler::GetPrefabFocusPathLength([[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
return aznumeric_cast<int>(m_instanceFocusVector.size());
return aznumeric_cast<int>(m_instanceFocusHierarchy.size());
}
void PrefabFocusHandler::OnEntityStreamLoadSuccess()
void PrefabFocusHandler::OnContextReset()
{
if (!m_isInitialized)
{
Initialize();
}
// Clear the old focus vector
m_instanceFocusVector.clear();
m_instanceFocusHierarchy.clear();
// Focus on the root prefab (AZ::EntityId() will default to it)
FocusOnPrefabInstanceOwningEntityId(AZ::EntityId());
}
void PrefabFocusHandler::OnEntityInfoUpdatedName(AZ::EntityId entityId, [[maybe_unused]]const AZStd::string& name)
{
// Determine if the entityId is the container for any of the instances in the vector.
auto result = AZStd::find_if(
m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(),
[entityId](const InstanceOptionalReference& instance)
{
return (instance->get().GetContainerEntityId() == entityId);
}
);
if (result != m_instanceFocusHierarchy.end())
{
// Refresh the path and notify changes.
RefreshInstanceFocusPath();
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
}
void PrefabFocusHandler::OnPrefabInstancePropagationEnd()
{
// Refresh the path and notify changes in case propagation updated any container names.
RefreshInstanceFocusPath();
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
void PrefabFocusHandler::OnPrefabTemplateDirtyFlagUpdated(TemplateId templateId, [[maybe_unused]] bool status)
{
// Determine if the templateId matches any of the instances in the vector.
auto result = AZStd::find_if(
m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(),
[templateId](const InstanceOptionalReference& instance)
{
return (instance->get().GetTemplateId() == templateId);
}
);
if (result != m_instanceFocusHierarchy.end())
{
// Refresh the path and notify changes.
RefreshInstanceFocusPath();
PrefabFocusNotificationBus::Broadcast(&PrefabFocusNotifications::OnPrefabFocusChanged);
}
}
void PrefabFocusHandler::RefreshInstanceFocusList()
{
m_instanceFocusVector.clear();
m_instanceFocusPath.clear();
m_instanceFocusHierarchy.clear();
AZStd::list<InstanceOptionalReference> instanceFocusList;
// Use a support list to easily push front while traversing the prefab hierarchy
InstanceOptionalReference currentInstance = m_focusedInstance;
while (currentInstance.has_value())
{
instanceFocusList.push_front(currentInstance);
m_instanceFocusHierarchy.emplace_back(currentInstance);
currentInstance = currentInstance->get().GetParentInstance();
}
// Populate internals using the support list
for (auto& instance : instanceFocusList)
// Invert the vector, since we need the top instance to be at index 0.
AZStd::reverse(m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end());
}
void PrefabFocusHandler::RefreshInstanceFocusPath()
{
auto prefabSystemComponentInterface = AZ::Interface<PrefabSystemComponentInterface>::Get();
m_instanceFocusPath.clear();
size_t index = 0;
size_t maxIndex = m_instanceFocusHierarchy.size() - 1;
for (const InstanceOptionalReference& instance : m_instanceFocusHierarchy)
{
m_instanceFocusPath.Append(instance->get().GetContainerEntity()->get().GetName());
m_instanceFocusVector.emplace_back(instance);
AZStd::string prefabName;
if (index < maxIndex)
{
// Get the filename without the extension (stem).
prefabName = instance->get().GetTemplateSourcePath().Stem().Native();
}
else
{
// Get the full filename.
prefabName = instance->get().GetTemplateSourcePath().Filename().Native();
}
if (prefabSystemComponentInterface->IsTemplateDirty(instance->get().GetTemplateId()))
{
prefabName += "*";
}
m_instanceFocusPath.Append(prefabName);
++index;
}
}
void PrefabFocusHandler::OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
{
return;
}
for (const InstanceOptionalReference& instance : instances)
{
if (instance.has_value())
@@ -274,6 +369,12 @@ namespace AzToolsFramework::Prefab
void PrefabFocusHandler::CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
{
return;
}
for (const InstanceOptionalReference& instance : instances)
{
if (instance.has_value())
@@ -11,9 +11,11 @@
#include <AzCore/Memory/SystemAllocator.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/FocusMode/FocusModeInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabPublicNotificationBus.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
namespace AzToolsFramework
@@ -30,7 +32,9 @@ namespace AzToolsFramework::Prefab
class PrefabFocusHandler final
: private PrefabFocusInterface
, private PrefabFocusPublicInterface
, private PrefabPublicNotificationBus::Handler
, private EditorEntityContextNotificationBus::Handler
, private EditorEntityInfoNotificationBus::Handler
{
public:
AZ_CLASS_ALLOCATOR(PrefabFocusHandler, AZ::SystemAllocator, 0);
@@ -38,9 +42,8 @@ namespace AzToolsFramework::Prefab
PrefabFocusHandler();
~PrefabFocusHandler();
void Initialize();
// PrefabFocusInterface overrides ...
void InitializeEditorInterfaces() override;
PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) override;
TemplateId GetFocusedPrefabTemplateId(AzFramework::EntityContextId entityContextId) const override;
InstanceOptionalReference GetFocusedPrefabInstance(AzFramework::EntityContextId entityContextId) const override;
@@ -50,29 +53,40 @@ namespace AzToolsFramework::Prefab
PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override;
AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const override;
bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const override;
bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const override;
const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const override;
const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override;
// EditorEntityContextNotificationBus overrides ...
void OnEntityStreamLoadSuccess() override;
void OnContextReset() override;
// EditorEntityInfoNotificationBus overrides ...
void OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZStd::string& name) override;
// PrefabPublicNotifications overrides ...
void OnPrefabInstancePropagationEnd() override;
void OnPrefabTemplateDirtyFlagUpdated(TemplateId templateId, bool status) override;
private:
PrefabFocusOperationResult FocusOnPrefabInstance(InstanceOptionalReference focusedInstance);
void RefreshInstanceFocusList();
void RefreshInstanceFocusPath();
void OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
void CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
//! The instance the editor is currently focusing on.
InstanceOptionalReference m_focusedInstance;
//! The templateId of the focused instance.
TemplateId m_focusedTemplateId;
AZStd::vector<InstanceOptionalReference> m_instanceFocusVector;
//! The list of instances going from the root (index 0) to the focused instance.
AZStd::vector<InstanceOptionalReference> m_instanceFocusHierarchy;
//! A path containing the names of the containers in the instance focus hierarchy, separated with a /.
AZ::IO::Path m_instanceFocusPath;
ContainerEntityInterface* m_containerEntityInterface = nullptr;
FocusModeInterface* m_focusModeInterface = nullptr;
InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr;
bool m_isInitialized = false;
};
} // namespace AzToolsFramework::Prefab
@@ -26,6 +26,11 @@ namespace AzToolsFramework::Prefab
public:
AZ_RTTI(PrefabFocusInterface, "{F3CFA37B-5FD8-436A-9C30-60EB54E350E1}");
//! Initializes the editor interfaces for Prefab Focus mode.
//! If this is not called on initialization, the Prefab Focus Mode functions will still work
//! but won't trigger the Editor APIs to visualize focus mode on the UI.
virtual void InitializeEditorInterfaces() = 0;
//! Set the focused prefab instance to the owning instance of the entityId provided.
//! @param entityId The entityId of the entity whose owning instance we want the prefab system to focus on.
virtual PrefabFocusOperationResult FocusOnPrefabInstanceOwningEntityId(AZ::EntityId entityId) = 0;
@@ -37,10 +37,15 @@ namespace AzToolsFramework::Prefab
//! Returns the entity id of the container entity for the instance the prefab system is focusing on.
virtual AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const = 0;
//! Returns whether the entity belongs to the instance that is being focused on.
//! @param entityId The entityId of the queried entity.
//! @return true if the entity belongs to the focused instance, false otherwise.
virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0;
//! Returns whether the entity belongs to the instance that is being focused on, or one of its descendants.
//! @param entityId The entityId of the queried entity.
//! @return true if the entity belongs to the focused instance or one of its descendants, false otherwise.
virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0;
virtual bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const = 0;
//! Returns the path from the root instance to the currently focused instance.
//! @return A path composed from the names of the container entities for the instance path.
@@ -974,7 +974,7 @@ namespace AzToolsFramework
return DeleteFromInstance(entityIds, true);
}
PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
DuplicatePrefabResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
{
if (entityIds.empty())
{
@@ -1021,6 +1021,7 @@ namespace AzToolsFramework
ScopedUndoBatch undoBatch("Duplicate Entities");
EntityIdList duplicatedEntityAndInstanceIds;
{
AZ_PROFILE_SCOPE(AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities");
@@ -1033,7 +1034,7 @@ namespace AzToolsFramework
if (!retrieveEntitiesAndInstancesOutcome.IsSuccess())
{
return AZStd::move(retrieveEntitiesAndInstancesOutcome);
return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError());
}
// Take a snapshot of the instance DOM before we manipulate it
@@ -1044,8 +1045,6 @@ namespace AzToolsFramework
PrefabDom instanceDomAfter;
instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator());
EntityIdList duplicatedEntityAndInstanceIds;
// Duplicate any nested entities and instances as requested
AZStd::unordered_map<InstanceAlias, Instance*> newInstanceAliasToOldInstanceMap;
AZStd::unordered_map<EntityAlias, EntityAlias> duplicateEntityAliasMap;
@@ -1114,7 +1113,7 @@ namespace AzToolsFramework
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds);
}
return AZ::Success();
return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds));
}
PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants)
@@ -63,7 +63,7 @@ namespace AzToolsFramework
PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override;
@@ -26,6 +26,7 @@ namespace AzToolsFramework
{
typedef AZ::Outcome<AZ::EntityId, AZStd::string> CreatePrefabResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> InstantiatePrefabResult;
typedef AZ::Outcome<EntityIdList, AZStd::string> DuplicatePrefabResult;
typedef AZ::Outcome<void, AZStd::string> PrefabOperationResult;
typedef AZ::Outcome<bool, AZStd::string> PrefabRequestResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> PrefabEntityResult;
@@ -160,14 +161,15 @@ namespace AzToolsFramework
/**
* Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance.
* @param entities The entities to duplicate.
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
* @return An outcome object with a list of ids of target entities' duplicates if duplication succeeded;
* on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
/**
* If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting
* the container entity into a regular entity and putting it under the parent prefab, removing the link between this
* instance and the parent, removing links between this instance and it's nested instances, adding entities directly
* instance and the parent, removing links between this instance and its nested instances, and adding entities directly
* owned by this instance under the parent instance.
* Bails if the entity is not a container entity or belongs to the level prefab instance.
* @param containerEntityId The container entity id of the instance to detach.
@@ -9,6 +9,7 @@
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzToolsFramework/Prefab/PrefabIdTypes.h>
namespace AzToolsFramework
{
@@ -22,6 +23,9 @@ namespace AzToolsFramework
virtual void OnPrefabInstancePropagationBegin() {}
virtual void OnPrefabInstancePropagationEnd() {}
virtual void OnPrefabTemplateDirtyFlagUpdated(
[[maybe_unused]] TemplateId templateId, [[maybe_unused]] bool status) {}
};
using PrefabPublicNotificationBus = AZ::EBus<PrefabPublicNotifications>;
@@ -25,6 +25,7 @@ namespace AzToolsFramework
{
using CreatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using InstantiatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using DuplicatePrefabResult = AZ::Outcome<EntityIdList, AZStd::string>;
using PrefabOperationResult = AZ::Outcome<void, AZStd::string>;
/**
@@ -69,6 +70,29 @@ namespace AzToolsFramework
* Return an outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0;
/**
* If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting
* the container entity into a regular entity and putting it under the parent prefab, removing the link between this
* instance and the parent, removing links between this instance and its nested instances, and adding entities directly
* owned by this instance under the parent instance.
* Bails if the entity is not a container entity or belongs to the level prefab instance.
* Return an outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0;
/**
* Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance.
* Return an outcome object with a list of ids of given entities' duplicates if duplication succeeded;
* on failure, it comes with an error message detailing the cause of the error.
*/
virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
/**
* Get the file path to the prefab file for the prefab instance owning the entity provided.
* Returns the path to the prefab, or an empty path if the entity is owned by the level.
*/
virtual AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const = 0;
};
using PrefabPublicRequestBus = AZ::EBus<PrefabPublicRequests>;
@@ -28,6 +28,9 @@ namespace AzToolsFramework
->Event("CreatePrefabInMemory", &PrefabPublicRequests::CreatePrefabInMemory)
->Event("InstantiatePrefab", &PrefabPublicRequests::InstantiatePrefab)
->Event("DeleteEntitiesAndAllDescendantsInInstance", &PrefabPublicRequests::DeleteEntitiesAndAllDescendantsInInstance)
->Event("DetachPrefab", &PrefabPublicRequests::DetachPrefab)
->Event("DuplicateEntitiesInInstance", &PrefabPublicRequests::DuplicateEntitiesInInstance)
->Event("GetOwningInstancePrefabPath", &PrefabPublicRequests::GetOwningInstancePrefabPath)
;
}
}
@@ -62,5 +65,19 @@ namespace AzToolsFramework
return m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entityIds);
}
PrefabOperationResult PrefabPublicRequestHandler::DetachPrefab(const AZ::EntityId& containerEntityId)
{
return m_prefabPublicInterface->DetachPrefab(containerEntityId);
}
DuplicatePrefabResult PrefabPublicRequestHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
{
return m_prefabPublicInterface->DuplicateEntitiesInInstance(entityIds);
}
AZStd::string PrefabPublicRequestHandler::GetOwningInstancePrefabPath(AZ::EntityId entityId) const
{
return m_prefabPublicInterface->GetOwningInstancePrefabPath(entityId).Native();
}
} // namespace Prefab
} // namespace AzToolsFramework
@@ -34,6 +34,9 @@ namespace AzToolsFramework
CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override;
InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override;
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override;
DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const override;
private:
PrefabPublicInterface* m_prefabPublicInterface = nullptr;
@@ -185,7 +185,7 @@ namespace AzToolsFramework
if (AZ::JsonSerialization::Compare(templateDomToUpdate, updatedDom) != AZ::JsonSerializerCompareResult::Equal)
{
templateDomToUpdate.CopyFrom(updatedDom, templateDomToUpdate.GetAllocator());
templateToUpdate->get().MarkAsDirty(true);
SetTemplateDirtyFlag(templateId, true);
PropagateTemplateChanges(templateId);
}
}
@@ -813,11 +813,12 @@ namespace AzToolsFramework
void PrefabSystemComponent::SetTemplateDirtyFlag(TemplateId templateId, bool dirty)
{
auto templateRef = FindTemplate(templateId);
if (templateRef.has_value())
if (auto templateReference = FindTemplate(templateId); templateReference.has_value())
{
templateRef->get().MarkAsDirty(dirty);
templateReference->get().MarkAsDirty(dirty);
PrefabPublicNotificationBus::Broadcast(
&PrefabPublicNotificationBus::Events::OnPrefabTemplateDirtyFlagUpdated, templateId, dirty);
}
}
@@ -23,7 +23,7 @@ namespace AzToolsFramework
}
//PrefabInstanceUndo
PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation)
PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation)
: PrefabUndoBase(undoOperationName)
{
m_useImmediatePropagation = useImmediatePropagation;
@@ -45,7 +45,7 @@ namespace AzToolsFramework
: public PrefabUndoBase
{
public:
explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true);
explicit PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation = true);
void Capture(
const PrefabDom& initialState,
@@ -66,7 +66,16 @@ namespace AzToolsFramework
bool Template::IsValid() const
{
return !m_prefabDom.IsNull() && !m_filePath.empty();
if (m_prefabDom.IsNull() || m_filePath.empty())
{
return false;
}
else if (!m_prefabDom.IsObject())
{
return false;
}
auto source = m_prefabDom.FindMember(PrefabDomUtils::SourceName);
return (source != m_prefabDom.MemberEnd());
}
bool Template::IsLoadedWithErrors() const
@@ -175,6 +184,26 @@ namespace AzToolsFramework
return findInstancesResult->get();
}
bool Template::IsProcedural() const
{
if (m_isProcedural.has_value())
{
return m_isProcedural.value();
}
else if (!IsValid())
{
return false;
}
auto source = m_prefabDom.FindMember(PrefabDomUtils::SourceName);
if (!source->value.IsString())
{
return false;
}
AZ::IO::PathView path(source->value.GetString());
m_isProcedural = AZStd::make_optional(path.Extension().Match(".procprefab"));
return m_isProcedural.value();
}
const AZ::IO::Path& Template::GetFilePath() const
{
return m_filePath;
@@ -65,6 +65,9 @@ namespace AzToolsFramework
const AZ::IO::Path& GetFilePath() const;
void SetFilePath(const AZ::IO::PathView& path);
// To tell if this Template was created from an product asset
bool IsProcedural() const;
private:
// Container for keeping links representing the Template's nested instances.
Links m_links;
@@ -80,6 +83,9 @@ namespace AzToolsFramework
// Flag to tell if this Template has changes that have yet to be saved to file.
bool m_isDirty = false;
// Flag to tell if this Template was generated outside the Editor
mutable AZStd::optional<bool> m_isProcedural;
};
} // namespace Prefab
} // namespace AzToolsFramework
@@ -2648,16 +2648,15 @@ namespace AzToolsFramework
}
else
{
QString cleanSaveAs(QDir::cleanPath(slicePath));
AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(slicePath.toUtf8().constData()).LexicallyNormal();
bool isPathSafeForAssets = false;
for (AZStd::string assetSafeFolder : assetSafeFolders)
for (const AZStd::string& assetSafeFolder : assetSafeFolders)
{
QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str()));
// Compare using clean paths so slash direction does not matter.
// Note that this comparison is case sensitive because some file systems
// Open 3D Engine supports are case sensitive.
if (cleanSaveAs.startsWith(cleanAssetSafeFolder))
AZ::IO::PathView assetSafeFolderView(assetSafeFolder);
// Check if the slice path is relative to the safe asset directory.
// The Path classes are being used to make this check case insensitive.
if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView))
{
isPathSafeForAssets = true;
break;
@@ -20,6 +20,8 @@
#include <AzFramework/Process/ProcessWatcher.h>
#include <AzToolsFramework/SourceControl/PerforceConnection.h>
#include <QProcess>
namespace AzToolsFramework
{
namespace
@@ -75,6 +77,7 @@ namespace AzToolsFramework
m_resolveKey = true;
m_testTrust = false;
// set up signals before we start thread.
m_shutdownThreadSignal = false;
m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this));
@@ -12,6 +12,7 @@
#include <AzCore/Component/Component.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option") // 4251: 'QLayoutItem::align': class 'QFlags<Qt::AlignmentFlag>' needs to have dll-interface to be used by clients of class 'QLayoutItem'
#include <AzToolsFramework/UI/SearchWidget/SearchCriteriaWidget.hxx>
AZ_POP_DISABLE_WARNING
@@ -20,67 +21,6 @@ namespace AzToolsFramework
{
namespace ComponentPaletteUtil
{
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter,
const AZStd::vector<AZ::ComponentServiceType>& incompatibleServiceFilter
)
{
AZ_Assert(componentClass, "Component class must not be null");
if (!componentClass)
{
return false;
}
AZ::ComponentDescriptor* componentDescriptor = nullptr;
EBUS_EVENT_ID_RESULT(componentDescriptor, componentClass->m_typeId, AZ::ComponentDescriptorBus, GetDescriptor);
if (!componentDescriptor)
{
return false;
}
// If no services are provided, this function returns true
if (serviceFilter.empty())
{
return true;
}
AZ::ComponentDescriptor::DependencyArrayType providedServices;
componentDescriptor->GetProvidedServices(providedServices, nullptr);
//reject this component if it does not offer any of the required services
if (AZStd::find_first_of(
providedServices.begin(),
providedServices.end(),
serviceFilter.begin(),
serviceFilter.end()) == providedServices.end())
{
return false;
}
//reject this component if it does offer any of the incompatible services
if (AZStd::find_first_of(
providedServices.begin(),
providedServices.end(),
incompatibleServiceFilter.begin(),
incompatibleServiceFilter.end()) != providedServices.end())
{
return false;
}
return true;
}
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter
)
{
const AZStd::vector<AZ::ComponentServiceType> incompatibleServices;
return OffersRequiredServices(componentClass, serviceFilter, incompatibleServices);
}
bool IsAddableByUser(const AZ::SerializeContext::ClassData* componentClass)
{
AZ_Assert(componentClass, "component class must not be null");
@@ -26,18 +26,6 @@ namespace AzToolsFramework
using ComponentIconTable = AZStd::map<const AZ::SerializeContext::ClassData*, QString>;
// Returns true if the given component provides at least one of the services specified or no services are provided
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter,
const AZStd::vector<AZ::ComponentServiceType>& incompatibleServiceFilter
);
bool OffersRequiredServices(
const AZ::SerializeContext::ClassData* componentClass,
const AZStd::vector<AZ::ComponentServiceType>& serviceFilter
);
// Returns true if the given component is addable by the user
bool IsAddableByUser(const AZ::SerializeContext::ClassData* componentClass);
@@ -101,8 +101,25 @@ namespace AzToolsFramework
{
}
void EditorEntityUiHandlerBase::OnDoubleClick([[maybe_unused]] AZ::EntityId entityId) const
bool EditorEntityUiHandlerBase::OnOutlinerItemClick(
[[maybe_unused]] const QPoint& position,
[[maybe_unused]] const QStyleOptionViewItem& option,
[[maybe_unused]] const QModelIndex& index) const
{
return false;
}
void EditorEntityUiHandlerBase::OnOutlinerItemExpand([[maybe_unused]] const QModelIndex& index) const
{
}
void EditorEntityUiHandlerBase::OnOutlinerItemCollapse([[maybe_unused]] const QModelIndex& index) const
{
}
bool EditorEntityUiHandlerBase::OnEntityDoubleClick([[maybe_unused]] AZ::EntityId entityId) const
{
return false;
}
} // namespace AzToolsFramework
@@ -61,8 +61,17 @@ namespace AzToolsFramework
virtual void PaintDescendantForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index,
const QModelIndex& descendantIndex) const;
//! Triggered when the entity is double clicked in the Outliner.
virtual void OnDoubleClick(AZ::EntityId entityId) const;
//! Triggered when the entity is clicked in the Outliner.
//! @return True if the click has been handled and should not be propagated, false otherwise.
virtual bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const;
//! Triggered when an entity's children are expanded in the Outliner.
virtual void OnOutlinerItemExpand(const QModelIndex& index) const;
//! Triggered when an entity's children are collapsed in the Outliner.
virtual void OnOutlinerItemCollapse(const QModelIndex& index) const;
//! Triggered when the entity is double clicked in the Outliner or in the Viewport.
//! @return True if the double click has been handled and should not be propagated, false otherwise.
virtual bool OnEntityDoubleClick(AZ::EntityId entityId) const;
private:
EditorEntityUiHandlerId m_handlerId = 0;
@@ -0,0 +1,91 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzCore/EBus/EBus.h>
#include <AzCore/Component/EntityId.h>
#include <AzQtComponents/Components/ToastNotificationConfiguration.h>
#include <QPoint>
#endif
namespace AzToolsFramework
{
typedef AZ::EntityId ToastId;
/**
* An EBus for receiving notifications when a user interacts with or dismisses
* a toast notification.
*/
class ToastNotifications
: public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ToastId;
virtual void OnToastInteraction() {}
virtual void OnToastDismissed() {}
};
using ToastNotificationBus = AZ::EBus<ToastNotifications>;
typedef AZ::u32 ToastRequestBusId;
/**
* An EBus used to hide or show toast notifications. Generally, these request are handled by a
* ToastNotificationsView that has been created with a specific ToastRequestBusId
* e.g. AZ_CRC("ExampleToastNotificationView")
*/
class ToastRequests
: public AZ::EBusTraits
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById;
using BusIdType = ToastRequestBusId; // bus is addressed by CRC of the view name
/**
* Hide a toast notification widget.
*
* @param toastId The toast notification's ToastId
*/
virtual void HideToastNotification(const ToastId& toastId) = 0;
/**
* Show a toast notification with the specified toast configuration. When handled by a ToastNotificationsView,
* notifications are queued and presented to the user in sequence.
*
* @param toastConfiguration The toast configuration
* @return a ToastId
*/
virtual ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0;
/**
* Show a toast notification with the specified toast configuration at the current moust cursor location.
*
* @param toastConfiguration The toast configuration
* @return a ToastId
*/
virtual ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) = 0;
/**
* Show a toast notification with the specified toast configuration at the specified location.
*
* @param screenPosition The screen position
* @param anchorPoint The anchorPoint for the toast notification widget
* @param toastConfiguration The toast configuration
* @return a ToastId
*/
virtual ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration&) = 0;
};
using ToastRequestBus = AZ::EBus<ToastRequests>;
}
@@ -0,0 +1,190 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzToolsFramework/UI/Notifications/ToastNotificationsView.h>
#include <AzQtComponents/Components/ToastNotification.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/functional.h>
namespace AzToolsFramework
{
ToastNotificationsView::ToastNotificationsView(QWidget* parent, ToastRequestBusId busId)
: QWidget(parent)
{
ToastRequestBus::Handler::BusConnect(busId);
}
ToastNotificationsView::~ToastNotificationsView()
{
ToastRequestBus::Handler::BusDisconnect();
}
void ToastNotificationsView::OnHide()
{
QWidget::hide();
if (m_activeNotification.IsValid())
{
auto notificationIter = m_notifications.find(m_activeNotification);
if (notificationIter != m_notifications.end())
{
notificationIter->second->hide();
}
}
}
void ToastNotificationsView::UpdateToastPosition()
{
if (m_activeNotification.IsValid())
{
auto notificationIter = m_notifications.find(m_activeNotification);
if (notificationIter != m_notifications.end())
{
notificationIter->second->UpdatePosition(GetGlobalPoint(), m_anchorPoint);
}
}
}
void ToastNotificationsView::OnShow()
{
QWidget::show();
if (m_activeNotification.IsValid() || !m_queuedNotifications.empty())
{
DisplayQueuedNotification();
}
}
ToastId ToastNotificationsView::ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration)
{
ToastId toastId = CreateToastNotification(toastConfiguration);
m_queuedNotifications.emplace_back(toastId);
if (!m_activeNotification.IsValid())
{
DisplayQueuedNotification();
}
return toastId;
}
ToastId ToastNotificationsView::ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration)
{
ToastId toastId = CreateToastNotification(toastConfiguration);
m_notifications[toastId]->ShowToastAtCursor();
return toastId;
}
ToastId ToastNotificationsView::ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration& toastConfiguration)
{
ToastId toastId = CreateToastNotification(toastConfiguration);
m_notifications[toastId]->ShowToastAtPoint(screenPosition, anchorPoint);
return toastId;
}
void ToastNotificationsView::HideToastNotification(const ToastId& toastId)
{
auto notificationIter = m_notifications.find(toastId);
if (notificationIter != m_notifications.end())
{
auto queuedIter = AZStd::find(m_queuedNotifications.begin(), m_queuedNotifications.end(), toastId);
if (queuedIter != m_queuedNotifications.end())
{
m_queuedNotifications.erase(queuedIter);
}
notificationIter->second->reject();
}
}
ToastId ToastNotificationsView::CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration)
{
AzQtComponents::ToastNotification* notification = aznew AzQtComponents::ToastNotification(parentWidget(), toastConfiguration);
ToastId toastId = AZ::Entity::MakeId();
m_notifications[toastId] = notification;
QObject::connect(
m_notifications[toastId], &AzQtComponents::ToastNotification::ToastNotificationHidden,
[toastId]()
{
ToastNotificationBus::Event(toastId, &ToastNotificationBus::Events::OnToastDismissed);
});
QObject::connect(
m_notifications[toastId], &AzQtComponents::ToastNotification::ToastNotificationInteraction,
[toastId]()
{
ToastNotificationBus::Event(toastId, &ToastNotificationBus::Events::OnToastInteraction);
});
return toastId;
}
QPoint ToastNotificationsView::GetGlobalPoint()
{
QPoint relativePoint = m_offset;
AZ_Assert(parentWidget(), "ToastNotificationsView has invalid parent QWidget");
if (m_anchorPoint.x() == 1.0)
{
relativePoint.setX(parentWidget()->width() - m_offset.x());
}
if (m_anchorPoint.y() == 1.0)
{
relativePoint.setY(parentWidget()->height() - m_offset.y());
}
return parentWidget()->mapToGlobal(relativePoint);
}
void ToastNotificationsView::DisplayQueuedNotification()
{
AZ_Assert(parentWidget(), "ToastNotificationsView has invalid parent QWidget");
if (m_queuedNotifications.empty() || !parentWidget()->isVisible() || !isVisible())
{
return;
}
ToastId toastId = m_queuedNotifications.front();
m_queuedNotifications.erase(m_queuedNotifications.begin());
auto notificationIter = m_notifications.find(toastId);
if (notificationIter != m_notifications.end())
{
m_activeNotification = toastId;
notificationIter->second->ShowToastAtPoint(GetGlobalPoint(), m_anchorPoint);
QObject::connect(
notificationIter->second, &AzQtComponents::ToastNotification::ToastNotificationHidden,
[&]()
{
m_activeNotification.SetInvalid();
DisplayQueuedNotification();
}
);
}
// If we didn't actually show something, recurse to avoid things getting stuck in the queue.
if (!m_activeNotification.IsValid())
{
DisplayQueuedNotification();
}
}
void ToastNotificationsView::SetOffset(const QPoint& offset)
{
m_offset = offset;
}
void ToastNotificationsView::SetAnchorPoint(const QPointF& anchorPoint)
{
m_anchorPoint = anchorPoint;
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QWidget>
#include <QPoint>
#include <QPointF>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzToolsFramework/UI/Notifications/ToastBus.h>
#endif
namespace AzQtComponents
{
class ToastNotification;
}
namespace AzToolsFramework
{
/**
* \brief A QWidget that displays and manages a queue of toast notifications.
*
* This view must be updated by its parent when the parent widget is show, hidden, moved
* or resized because toast notifications are displayed on top of the parent and are not part
* of the layout, so they must be manually moved.
*/
class ToastNotificationsView final
: public QWidget
, protected ToastRequestBus::Handler
{
Q_OBJECT
public:
ToastNotificationsView(QWidget* parent, ToastRequestBusId busId);
~ToastNotificationsView() override;
void HideToastNotification(const ToastId& toastId) override;
ToastId ShowToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration) override;
ToastId ShowToastAtCursor(const AzQtComponents::ToastConfiguration& toastConfiguration) override;
ToastId ShowToastAtPoint(const QPoint& screenPosition, const QPointF& anchorPoint, const AzQtComponents::ToastConfiguration&) override;
void OnHide();
void OnShow();
void UpdateToastPosition();
void SetOffset(const QPoint& offset);
void SetAnchorPoint(const QPointF& anchorPoint);
private:
ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration);
void DisplayQueuedNotification();
QPoint GetGlobalPoint();
ToastId m_activeNotification;
AZStd::unordered_map<ToastId, AzQtComponents::ToastNotification*> m_notifications;
AZStd::vector<ToastId> m_queuedNotifications;
QPoint m_offset = QPoint(10, 10);
QPointF m_anchorPoint = QPointF(1, 0);
};
} // AzToolsFramework
@@ -11,10 +11,12 @@
#include <QApplication>
#include <QBitmap>
#include <QCheckBox>
#include <QEvent>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QMessageBox>
#include <QMimeData>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QStyle>
@@ -2287,7 +2289,14 @@ namespace AzToolsFramework
// Now we setup a Text Document so it can draw the rich text
QTextDocument textDoc;
textDoc.setDefaultFont(optionV4.font);
textDoc.setDefaultStyleSheet("body {color: white}");
if (option.state & QStyle::State_Enabled)
{
textDoc.setDefaultStyleSheet("body {color: white}");
}
else
{
textDoc.setDefaultStyleSheet("body {color: #7C7C7C}");
}
textDoc.setHtml("<body>" + entityNameRichText + "</body>");
painter->translate(textRect.topLeft());
textDoc.setTextWidth(textRect.width());
@@ -2326,6 +2335,23 @@ namespace AzToolsFramework
return true;
}
if (event->type() == QEvent::MouseButtonPress)
{
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
if (auto editorEntityUiInterface = AZ::Interface<EditorEntityUiInterface>::Get(); editorEntityUiInterface != nullptr)
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
auto entityUiHandler = editorEntityUiInterface->GetHandler(entityId);
if (entityUiHandler && entityUiHandler->OnOutlinerItemClick(mouseEvent->pos(), option, index))
{
return true;
}
}
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
@@ -73,6 +73,8 @@ namespace AzToolsFramework
void EntityOutlinerTreeView::leaveEvent([[maybe_unused]] QEvent* event)
{
m_mousePosition = QPoint();
m_currentHoveredIndex = QModelIndex();
update();
}
void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event)
@@ -129,6 +131,11 @@ namespace AzToolsFramework
}
m_mousePosition = event->pos();
if (QModelIndex hoveredIndex = indexAt(m_mousePosition); m_currentHoveredIndex != indexAt(m_mousePosition))
{
m_currentHoveredIndex = hoveredIndex;
update();
}
//process mouse movement as normal, potentially triggering drag and drop
QTreeView::mouseMoveEvent(event);
@@ -90,6 +90,8 @@ namespace AzToolsFramework
const QColor m_selectedColor = QColor(255, 255, 255, 45);
const QColor m_hoverColor = QColor(255, 255, 255, 30);
QModelIndex m_currentHoveredIndex;
EditorEntityUiInterface* m_editorEntityFrameworkInterface;
};
@@ -902,6 +902,7 @@ namespace AzToolsFramework
EditorPickModeRequestBus::Broadcast(
&EditorPickModeRequests::StopEntityPickMode);
return;
}
switch (index.column())
@@ -918,18 +919,30 @@ namespace AzToolsFramework
{
if (AZ::EntityId entityId = GetEntityIdFromIndex(index); auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId))
{
entityUiHandler->OnDoubleClick(entityId);
entityUiHandler->OnEntityDoubleClick(entityId);
}
}
void EntityOutlinerWidget::OnTreeItemExpanded(const QModelIndex& index)
{
m_listModel->OnEntityExpanded(GetEntityIdFromIndex(index));
AZ::EntityId entityId = GetEntityIdFromIndex(index);
if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId))
{
entityUiHandler->OnOutlinerItemExpand(index);
}
m_listModel->OnEntityExpanded(entityId);
}
void EntityOutlinerWidget::OnTreeItemCollapsed(const QModelIndex& index)
{
m_listModel->OnEntityCollapsed(GetEntityIdFromIndex(index));
AZ::EntityId entityId = GetEntityIdFromIndex(index);
if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId))
{
entityUiHandler->OnOutlinerItemCollapse(index);
}
m_listModel->OnEntityCollapsed(entityId);
}
void EntityOutlinerWidget::OnExpandEntity(const AZ::EntityId& entityId, bool expand)
@@ -1163,7 +1176,7 @@ namespace AzToolsFramework
{
QTimer::singleShot(1, this, [this]() {
m_gui->m_objectTree->setUpdatesEnabled(true);
m_gui->m_objectTree->expandToDepth(0);
m_gui->m_objectTree->expand(m_proxyModel->index(0,0));
});
}
@@ -27,6 +27,7 @@
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabLoaderInterface.h>
#include <AzToolsFramework/Prefab/Procedural/ProceduralPrefabAsset.h>
@@ -135,6 +136,10 @@ namespace AzToolsFramework
return;
}
// Initialize Editor functionality for the Prefab Focus Handler
auto prefabFocusInterface = AZ::Interface<PrefabFocusInterface>::Get();
prefabFocusInterface->InitializeEditorInterfaces();
EditorContextMenuBus::Handler::BusConnect();
EditorEventsBus::Handler::BusConnect();
PrefabInstanceContainerNotificationBus::Handler::BusConnect();
@@ -250,7 +255,7 @@ namespace AzToolsFramework
if (s_prefabPublicInterface->IsInstanceContainerEntity(selectedEntity))
{
// Edit Prefab
if (prefabWipFeaturesEnabled && !s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity))
if (!s_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(selectedEntity))
{
QAction* editAction = menu->addAction(QObject::tr("Edit Prefab"));
editAction->setToolTip(QObject::tr("Edit the prefab in focus mode."));
@@ -800,16 +805,15 @@ namespace AzToolsFramework
}
else
{
QString cleanSaveAs(QDir::cleanPath(prefabPath));
AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(prefabPath.toUtf8().constData()).LexicallyNormal();
bool isPathSafeForAssets = false;
for (AZStd::string assetSafeFolder : assetSafeFolders)
for (const AZStd::string& assetSafeFolder : assetSafeFolders)
{
QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str()));
// Compare using clean paths so slash direction does not matter.
// Note that this comparison is case sensitive because some file systems
// Open 3D Engine supports are case sensitive.
if (cleanSaveAs.startsWith(cleanAssetSafeFolder))
AZ::IO::PathView assetSafeFolderView(assetSafeFolder);
// Check if the prefabPath is relative to the safe asset directory.
// The Path classes are being used to make this check case insensitive.
if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView))
{
isPathSafeForAssets = true;
break;
@@ -1154,25 +1158,14 @@ namespace AzToolsFramework
{
s_editorEntityUiInterface->RegisterEntity(entityId, m_prefabUiHandler.GetHandlerId());
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled)
{
// Register entity as a container
s_containerEntityInterface->RegisterEntityAsContainer(entityId);
}
// Register entity as a container
s_containerEntityInterface->RegisterEntityAsContainer(entityId);
}
}
void PrefabIntegrationManager::OnPrefabComponentDeactivate(AZ::EntityId entityId)
{
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
if (prefabWipFeaturesEnabled && !s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId))
if (!s_prefabPublicInterface->IsLevelInstanceContainerEntity(entityId))
{
// Unregister entity as a container
s_containerEntityInterface->UnregisterEntityAsContainer(entityId);
@@ -21,10 +21,16 @@
namespace AzToolsFramework
{
const QColor PrefabUiHandler::m_backgroundColor = QColor("#444444");
const QColor PrefabUiHandler::m_backgroundHoverColor = QColor("#5A5A5A");
const QColor PrefabUiHandler::m_backgroundSelectedColor = QColor("#656565");
const QColor PrefabUiHandler::m_prefabCapsuleColor = QColor("#1E252F");
const QColor PrefabUiHandler::m_prefabCapsuleDisabledColor = QColor("#35383C");
const QColor PrefabUiHandler::m_prefabCapsuleEditColor = QColor("#4A90E2");
const QString PrefabUiHandler::m_prefabIconPath = QString(":/Entity/prefab.svg");
const QString PrefabUiHandler::m_prefabEditIconPath = QString(":/Entity/prefab_edit.svg");
const QString PrefabUiHandler::m_prefabEditOpenIconPath = QString(":/Entity/prefab_edit_open.svg");
const QString PrefabUiHandler::m_prefabEditCloseIconPath = QString(":/Entity/prefab_edit_close.svg");
PrefabUiHandler::PrefabUiHandler()
{
@@ -75,7 +81,7 @@ namespace AzToolsFramework
if (!path.empty())
{
tooltip = QObject::tr("%1").arg(path.Native().data());
tooltip = QObject::tr("Double click to edit.\n%1").arg(path.Native().data());
}
return tooltip;
@@ -102,13 +108,20 @@ namespace AzToolsFramework
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName;
const bool isLastColumn = index.column() == EntityOutlinerListModel::ColumnLockToggle;
const bool hasVisibleChildren = index.data(EntityOutlinerListModel::ExpandedRole).value<bool>() && index.model()->hasChildren(index);
QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName);
const bool hasVisibleChildren =
firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value<bool>() &&
firstColumnIndex.model()->hasChildren(firstColumnIndex);
QColor backgroundColor = m_prefabCapsuleColor;
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
backgroundColor = m_prefabCapsuleEditColor;
}
else if (!(option.state & QStyle::State_Enabled))
{
backgroundColor = m_prefabCapsuleDisabledColor;
}
QPainterPath backgroundPath;
backgroundPath.setFillRule(Qt::WindingFill);
@@ -184,7 +197,8 @@ namespace AzToolsFramework
const bool isFirstColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnName;
const bool isLastColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnLockToggle;
QColor borderColor = m_prefabCapsuleColor;
// There is no legal way of opening prefabs in their default state, so default to disabled.
QColor borderColor = m_prefabCapsuleDisabledColor;
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
borderColor = m_prefabCapsuleEditColor;
@@ -273,6 +287,71 @@ namespace AzToolsFramework
painter->restore();
}
void PrefabUiHandler::PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, [[maybe_unused]] const QModelIndex& index) const
{
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
const QPoint offset = QPoint(-18, 3);
QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName);
const int iconSize = 16;
const bool isHovered = (option.state & QStyle::State_MouseOver);
const bool isSelected = index.data(EntityOutlinerListModel::SelectedRole).template value<bool>();
const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName;
const bool isExpanded =
firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value<bool>() &&
firstColumnIndex.model()->hasChildren(firstColumnIndex);
if (!isFirstColumn || !(option.state & QStyle::State_Enabled))
{
return;
}
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
// Only show the close icon if the prefab is expanded.
// This allows the prefab container to be opened if it was collapsed during propagation.
if (!isExpanded)
{
return;
}
// Use the same color as the background.
QColor backgroundColor = m_backgroundColor;
if (isSelected)
{
backgroundColor = m_backgroundSelectedColor;
}
else if (isHovered)
{
backgroundColor = m_backgroundHoverColor;
}
// Paint a rect to cover up the expander.
QRect rect = QRect(0, 0, 16, 16);
rect.translate(option.rect.topLeft() + offset);
painter->fillRect(rect, backgroundColor);
// Paint the icon.
QIcon closeIcon = QIcon(m_prefabEditCloseIconPath);
painter->drawPixmap(option.rect.topLeft() + offset, closeIcon.pixmap(iconSize));
}
else
{
// Only show the edit icon on hover.
if (!isHovered)
{
return;
}
QIcon openIcon = QIcon(m_prefabEditOpenIconPath);
painter->drawPixmap(option.rect.topLeft() + offset, openIcon.pixmap(iconSize));
}
painter->restore();
}
bool PrefabUiHandler::IsLastVisibleChild(const QModelIndex& parent, const QModelIndex& child)
{
QModelIndex lastVisibleItemIndex = GetLastVisibleChild(parent);
@@ -314,16 +393,53 @@ namespace AzToolsFramework
return Internal_GetLastVisibleChild(model, lastChild);
}
void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const
bool PrefabUiHandler::OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
bool prefabWipFeaturesEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
prefabWipFeaturesEnabled, &AzFramework::ApplicationRequests::ArePrefabWipFeaturesEnabled);
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
const QPoint offset = QPoint(-18, 3);
if (prefabWipFeaturesEnabled)
if (m_prefabFocusPublicInterface->IsOwningPrefabInFocusHierarchy(entityId))
{
// Focus on this prefab
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
QRect iconRect = QRect(0, 0, 16, 16);
iconRect.translate(option.rect.topLeft() + offset);
if (iconRect.contains(position))
{
if (!m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
// Focus on this prefab.
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
}
// Don't propagate event.
return true;
}
}
return false;
}
void PrefabUiHandler::OnOutlinerItemCollapse(const QModelIndex& index) const
{
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
auto editorEntityContextId = AzFramework::EntityContextId::CreateNull();
EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
// Go one level up.
int length = m_prefabFocusPublicInterface->GetPrefabFocusPathLength(editorEntityContextId);
m_prefabFocusPublicInterface->FocusOnPathIndex(editorEntityContextId, length - 2);
}
}
bool PrefabUiHandler::OnEntityDoubleClick(AZ::EntityId entityId) const
{
// Focus on this prefab
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
// Don't propagate event.
return true;
}
}
@@ -36,7 +36,10 @@ namespace AzToolsFramework
void PaintItemBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void PaintDescendantBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index,
const QModelIndex& descendantIndex) const override;
void OnDoubleClick(AZ::EntityId entityId) const override;
void PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void OnOutlinerItemCollapse(const QModelIndex& index) const override;
bool OnEntityDoubleClick(AZ::EntityId entityId) const override;
private:
Prefab::PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
@@ -48,9 +51,15 @@ namespace AzToolsFramework
static constexpr int m_prefabCapsuleRadius = 6;
static constexpr int m_prefabBorderThickness = 2;
static const QColor m_backgroundColor;
static const QColor m_backgroundHoverColor;
static const QColor m_backgroundSelectedColor;
static const QColor m_prefabCapsuleColor;
static const QColor m_prefabCapsuleDisabledColor;
static const QColor m_prefabCapsuleEditColor;
static const QString m_prefabIconPath;
static const QString m_prefabEditIconPath;
static const QString m_prefabEditOpenIconPath;
static const QString m_prefabEditCloseIconPath;
};
} // namespace AzToolsFramework
@@ -10,6 +10,8 @@
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <QTimer>
namespace AzToolsFramework::Prefab
{
PrefabViewportFocusPathHandler::PrefabViewportFocusPathHandler()
@@ -47,6 +49,9 @@ namespace AzToolsFramework::Prefab
[&](const QString&, int linkIndex)
{
m_prefabFocusPublicInterface->FocusOnPathIndex(m_editorEntityContextId, linkIndex);
// Manually refresh path
QTimer::singleShot(0, [&]() { OnPrefabFocusChanged(); });
}
);
@@ -302,6 +302,12 @@ namespace AzToolsFramework
using EditorViewportInputTimeNowRequestBus = AZ::EBus<EditorViewportInputTimeNowRequests>;
//! The style of cursor override.
enum class CursorStyleOverride
{
Forbidden
};
//! Viewport requests for managing the viewport cursor state.
class ViewportMouseCursorRequests
{
@@ -312,6 +318,10 @@ namespace AzToolsFramework
virtual void EndCursorCapture() = 0;
//! Is the mouse over the viewport.
virtual bool IsMouseOver() const = 0;
//! Set the cursor style override.
virtual void SetOverrideCursor(CursorStyleOverride cursorStyleOverride) = 0;
//! Clear the cursor style override.
virtual void ClearOverrideCursor() = 0;
protected:
~ViewportMouseCursorRequests() = default;
@@ -44,6 +44,13 @@ AZ_CVAR(
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Display the aggregate world bounds for a given entity (the union of all world component Aabbs)");
AZ_CVAR(
bool,
ed_useCursorLockIconInFocusMode,
false,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Use a lock icon when the cursor is over entities that cannot be interacted with");
namespace AzToolsFramework
{
@@ -222,6 +229,13 @@ namespace AzToolsFramework
// verify if the entity Id corresponds to an entity that is focused; if not, halt selection.
if (entityIdUnderCursor.IsValid() && !IsSelectableAccordingToFocusMode(entityIdUnderCursor))
{
if (ed_useCursorLockIconInFocusMode)
{
ViewportInteraction::ViewportMouseCursorRequestBus::Event(
viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor,
ViewportInteraction::CursorStyleOverride::Forbidden);
}
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down ||
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick)
@@ -232,6 +246,9 @@ namespace AzToolsFramework
return CursorEntityIdQuery(AZ::EntityId(), AZ::EntityId());
}
ViewportInteraction::ViewportMouseCursorRequestBus::Event(
viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::ClearOverrideCursor);
// container entity support - if the entity that is being selected is part of a closed container,
// change the selection to the container instead.
if (ContainerEntityInterface* containerEntityInterface = AZ::Interface<ContainerEntityInterface>::Get())
@@ -20,7 +20,7 @@ namespace AzToolsFramework::ViewportUi::Internal
{
const static int HighlightBorderSize = 5;
const static int TopHighlightBorderSize = 25;
const static char* HighlightBorderColor = "#44B2F8";
const static char* HighlightBorderColor = "#4A90E2";
static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup)
{
@@ -759,6 +759,9 @@ set(FILES
UI/Prefab/PrefabUiHandler.cpp
UI/Prefab/PrefabViewportFocusPathHandler.h
UI/Prefab/PrefabViewportFocusPathHandler.cpp
UI/Notifications/ToastNotificationsView.cpp
UI/Notifications/ToastNotificationsView.h
UI/Notifications/ToastBus.h
PythonTerminal/ScriptHelpDialog.cpp
PythonTerminal/ScriptHelpDialog.h
PythonTerminal/ScriptHelpDialog.ui
@@ -22,6 +22,7 @@
#include <AzToolsFramework/Application/ToolsApplication.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/UnitTest/ToolsTestApplication.h>
// Inspector Test Includes
@@ -313,17 +314,17 @@ namespace UnitTest
AZ_TEST_ASSERT(testComponent1_ProvidedServices.size() == 1);
const AZ::SerializeContext::ClassData* testComponent1_ClassData = context->FindClassData(testComponent1_typeId);
EXPECT_TRUE(AzToolsFramework::ComponentPaletteUtil::OffersRequiredServices(testComponent1_ClassData, testComponent1_ProvidedServices));
EXPECT_TRUE(AzToolsFramework::OffersRequiredServices(testComponent1_ClassData, testComponent1_ProvidedServices));
// Verify that OffersRequiredServices returns when given services provided by a different component
AZ::ComponentDescriptor::DependencyArrayType testComponent2_ProvidedServices;
Inspector_TestComponent2::GetProvidedServices(testComponent2_ProvidedServices);
AZ_TEST_ASSERT(testComponent2_ProvidedServices.size() == 1);
AZ_TEST_ASSERT(testComponent1_ProvidedServices != testComponent2_ProvidedServices);
EXPECT_FALSE(AzToolsFramework::ComponentPaletteUtil::OffersRequiredServices(testComponent1_ClassData, testComponent2_ProvidedServices));
EXPECT_FALSE(AzToolsFramework::OffersRequiredServices(testComponent1_ClassData, testComponent2_ProvidedServices));
// verify that OffersRequiredServices returns true when provided with an empty list of services
EXPECT_TRUE(AzToolsFramework::ComponentPaletteUtil::OffersRequiredServices(testComponent1_ClassData, AZ::ComponentDescriptor::DependencyArrayType()));
EXPECT_TRUE(AzToolsFramework::OffersRequiredServices(testComponent1_ClassData, AZ::ComponentDescriptor::DependencyArrayType()));
//////////////////////////////////////////////////////////////////////////
// TEST IsAddableByUser()
@@ -14,6 +14,7 @@
#include <Prefab/PrefabTestFixture.h>
#include <Prefab/Procedural/ProceduralPrefabAsset.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzToolsFramework/Prefab/Template/Template.h>
namespace UnitTest
{
@@ -132,4 +133,32 @@ namespace UnitTest
EXPECT_TRUE(outputValue.HasMember("member"));
EXPECT_STREQ(outputValue.FindMember("member")->value.GetString(), "value");
}
TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_DefaultsToNotProcPrefab)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
dom.AddMember("Source", "foo.prefab", dom.GetAllocator());
AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom));
EXPECT_FALSE(fooTemplate.IsProcedural());
}
TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_DomDrivesFlagToTrue)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
dom.AddMember("Source", "foo.procprefab", dom.GetAllocator());
AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom));
EXPECT_TRUE(fooTemplate.IsProcedural());
// the second time should use the cached version of the flag
EXPECT_TRUE(fooTemplate.IsProcedural());
}
TEST_F(ProceduralPrefabAssetTest, Template_IsProcPrefab_FailsWithNoSource)
{
AzToolsFramework::Prefab::PrefabDom dom;
dom.SetObject();
AzToolsFramework::Prefab::Template fooTemplate("foo", AZStd::move(dom));
EXPECT_FALSE(fooTemplate.IsProcedural());
}
}