diff --git a/Assets/Editor/Translation/scriptcanvas_en_us.ts b/Assets/Editor/Translation/scriptcanvas_en_us.ts
index a7d29e2dbc..5cafabaf60 100644
--- a/Assets/Editor/Translation/scriptcanvas_en_us.ts
+++ b/Assets/Editor/Translation/scriptcanvas_en_us.ts
@@ -14679,6 +14679,25 @@ An Entity can be selected by using the pick button, or by dragging an Entity fro
+
+ Method: NetBindComponent
+
+ NETBINDCOMPONENT_ISNETENTITYROLEAUTHORITY_TOOLTIP
+ Returns true if this network entity is an authoritative proxy on a server (full authority); otherwise false.
+
+
+ NETBINDCOMPONENT_ISNETENTITYROLEAUTONOMOUS_TOOLTIP
+ Returns true if this network entity is an autonomous proxy on a client (can execute local prediction) or if this network entity is an authoritative proxy on a server but has autonomous privileges (ie: a host who is also a player); otherwise false.
+
+
+ NETBINDCOMPONENT_ISNETENTITYROLECLIENT_TOOLTIP
+ Returns true if this network entity is a simulated proxy on a client; otherwise false.
+
+
+ NETBINDCOMPONENT_ISNETENTITYROLESERVER_TOOLTIP
+ Returns true if this network entity is a simulated proxy on a server (ie: a different server may own this entity, but the entity has been replicated to this server; otherwise false.
+
+
Method: Math
diff --git a/Assets/Editor/UI/releaseTranslations.py b/Assets/Editor/UI/releaseTranslations.py
deleted file mode 100755
index 9ab0345d98..0000000000
--- a/Assets/Editor/UI/releaseTranslations.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
-# its licensors.
-#
-# For complete copyright and license terms please see the LICENSE at the root of this
-# distribution (the "License"). All use of this software is governed by the License,
-# or, if provided, by the license below or the license accompanying this file. Do not
-# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#
-import os
-
-os.chdir('qml')
-startDir = os.getcwd()
-
-# since it's a .exe file it will only work on windows, but we may as well
-# construct the path in a platform-independent way.
-lreleaseCmd = os.path.join(startDir, '..', '..', '..',
- 'Code', 'SDKs', 'Qt', 'x64', 'bin', 'lrelease.exe ')
-
-print(startDir)
-
-# Korean, Japanese and Simplified Chinese
-targetLanguages = ['ko', 'ja', 'zh_CN']
-
-for lang in targetLanguages:
- os.chdir(startDir)
- tgtLang = '-target-language ' + lang
- os.system(lreleaseCmd + 'this_' + lang + '.ts')
-
- for fileName in os.listdir():
- if not fileName.endswith(".ts"):
- continue
-
- os.system(lreleaseCmd + ' ' + fileName)
-
- print(('Finished processing: ' + fileName))
diff --git a/AutomatedTesting/Config/aws_resource_mappings.json b/AutomatedTesting/Config/default_aws_resource_mappings.json
similarity index 100%
rename from AutomatedTesting/Config/aws_resource_mappings.json
rename to AutomatedTesting/Config/default_aws_resource_mappings.json
diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py
index 511d9b3ecd..d8fd8b145a 100644
--- a/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py
+++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py
@@ -112,7 +112,7 @@ def remove_file(file_path: str) -> None:
@pytest.mark.parametrize('project', ['AutomatedTesting'])
@pytest.mark.parametrize('level', ['AWS/Metrics'])
@pytest.mark.parametrize('feature_name', [AWS_METRICS_FEATURE_NAME])
-@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json'])
+@pytest.mark.parametrize('resource_mappings_filename', ['default_aws_resource_mappings.json'])
@pytest.mark.parametrize('profile_name', ['AWSAutomationTest'])
@pytest.mark.parametrize('region_name', ['us-west-2'])
@pytest.mark.parametrize('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests'])
diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_anonymous_credentials.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_anonymous_credentials.py
index f8aa5b85eb..a477e89821 100644
--- a/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_anonymous_credentials.py
+++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_anonymous_credentials.py
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
@pytest.mark.usefixtures('cdk')
@pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME])
@pytest.mark.usefixtures('resource_mappings')
-@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json'])
+@pytest.mark.parametrize('resource_mappings_filename', ['default_aws_resource_mappings.json'])
@pytest.mark.usefixtures('aws_utils')
@pytest.mark.parametrize('region_name', ['us-west-2'])
@pytest.mark.parametrize('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests'])
diff --git a/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_password_signin.py b/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_password_signin.py
index 28b17fdeee..a0bcf56b0d 100644
--- a/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_password_signin.py
+++ b/AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/test_password_signin.py
@@ -35,7 +35,7 @@ logger = logging.getLogger(__name__)
@pytest.mark.usefixtures('cdk')
@pytest.mark.parametrize('feature_name', [AWS_CLIENT_AUTH_FEATURE_NAME])
@pytest.mark.usefixtures('resource_mappings')
-@pytest.mark.parametrize('resource_mappings_filename', ['aws_resource_mappings.json'])
+@pytest.mark.parametrize('resource_mappings_filename', ['default_aws_resource_mappings.json'])
@pytest.mark.usefixtures('aws_utils')
@pytest.mark.parametrize('region_name', ['us-west-2'])
@pytest.mark.parametrize('assume_role_arn', ['arn:aws:iam::645075835648:role/o3de-automation-tests'])
diff --git a/AutomatedTesting/Gem/PythonTests/editor/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/editor/CMakeLists.txt
index 834254134e..4441b4ac23 100644
--- a/AutomatedTesting/Gem/PythonTests/editor/CMakeLists.txt
+++ b/AutomatedTesting/Gem/PythonTests/editor/CMakeLists.txt
@@ -15,7 +15,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
TEST_SUITE main
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}
- PYTEST_MARKS "not SUITE_sandbox and not SUITE_periodic and not SUITE_benchmark"
+ PYTEST_MARKS "SUITE_main and not REQUIRES_gpu"
TIMEOUT 1500
RUNTIME_DEPENDENCIES
Legacy::Editor
@@ -30,7 +30,23 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_
TEST_SUITE periodic
TEST_SERIAL
PATH ${CMAKE_CURRENT_LIST_DIR}
- PYTEST_MARKS "SUITE_periodic"
+ PYTEST_MARKS "SUITE_periodic and not REQUIRES_gpu"
+ TIMEOUT 1500
+ RUNTIME_DEPENDENCIES
+ Legacy::Editor
+ AZ::AssetProcessor
+ AutomatedTesting.Assets
+ COMPONENT
+ Editor
+ )
+
+ ly_add_pytest(
+ NAME AutomatedTesting::EditorTests_Main_GPU
+ TEST_SUITE main
+ TEST_SERIAL
+ TEST_REQUIRES gpu
+ PATH ${CMAKE_CURRENT_LIST_DIR}
+ PYTEST_MARKS "SUITE_main and REQUIRES_gpu"
TIMEOUT 1500
RUNTIME_DEPENDENCIES
Legacy::Editor
diff --git a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/BasicEditorWorkflows_LevelEntityComponentCRUD.py b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/BasicEditorWorkflows_LevelEntityComponentCRUD.py
index 5d4218efd0..c195672760 100644
--- a/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/BasicEditorWorkflows_LevelEntityComponentCRUD.py
+++ b/AutomatedTesting/Gem/PythonTests/editor/EditorScripts/BasicEditorWorkflows_LevelEntityComponentCRUD.py
@@ -79,8 +79,6 @@ class TestBasicEditorWorkflows(EditorTestHelper):
grp_box = new_level_dlg.findChild(QtWidgets.QGroupBox, "STATIC_GROUP1")
level_name = grp_box.findChild(QtWidgets.QLineEdit, "LEVEL")
level_name.setText(self.args["level"])
- level_folders = grp_box.findChild(QtWidgets.QComboBox, "LEVEL_FOLDERS")
- level_folders.setCurrentText("Levels/")
button_box = new_level_dlg.findChild(QtWidgets.QDialogButtonBox, "buttonBox")
button_box.button(QtWidgets.QDialogButtonBox.Ok).click()
diff --git a/AutomatedTesting/Gem/PythonTests/editor/test_BasicEditorWorkflows.py b/AutomatedTesting/Gem/PythonTests/editor/test_BasicEditorWorkflows.py
index f65401f007..1f0842dbaa 100644
--- a/AutomatedTesting/Gem/PythonTests/editor/test_BasicEditorWorkflows.py
+++ b/AutomatedTesting/Gem/PythonTests/editor/test_BasicEditorWorkflows.py
@@ -66,3 +66,35 @@ class TestBasicEditorWorkflows(object):
timeout=log_monitor_timeout,
auto_test_mode=False
)
+
+ @pytest.mark.test_case_id("C6351273", "C6384955", "C16929880", "C15167490", "C15167491")
+ @pytest.mark.SUITE_main
+ @pytest.mark.REQUIRES_gpu
+ def test_BasicEditorWorkflows_GPU_LevelEntityComponentCRUD(self, request, editor, level, launcher_platform):
+
+ # Skip test if running against Debug build
+ if "debug" in internal_plugin.build_directory:
+ pytest.skip("Does not execute against debug builds.")
+
+ expected_lines = [
+ "Create and load new level: True",
+ "New entity creation: True",
+ "Create entity hierarchy: True",
+ "Add component: True",
+ "Component update: True",
+ "Remove component: True",
+ "Save and Export: True",
+ "BasicEditorWorkflows_LevelEntityComponent: result=SUCCESS",
+ ]
+
+ hydra.launch_and_validate_results(
+ request,
+ test_directory,
+ editor,
+ "BasicEditorWorkflows_LevelEntityComponentCRUD.py",
+ expected_lines,
+ cfg_args=[level],
+ timeout=log_monitor_timeout,
+ auto_test_mode=False,
+ null_renderer=False
+ )
diff --git a/AutomatedTesting/Registry/awscoreconfiguration.setreg b/AutomatedTesting/Registry/awscoreconfiguration.setreg
index b7c60b0fb9..1927909328 100644
--- a/AutomatedTesting/Registry/awscoreconfiguration.setreg
+++ b/AutomatedTesting/Registry/awscoreconfiguration.setreg
@@ -4,7 +4,7 @@
"AWSCore":
{
"ProfileName": "AWSAutomationTest",
- "ResourceMappingConfigFileName": "aws_resource_mappings.json"
+ "ResourceMappingConfigFileName": "default_aws_resource_mappings.json"
}
}
}
\ No newline at end of file
diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetTypeInfoBus.h b/Code/Framework/AzCore/AzCore/Asset/AssetTypeInfoBus.h
index 48115b53af..f49b3ff6e5 100644
--- a/Code/Framework/AzCore/AzCore/Asset/AssetTypeInfoBus.h
+++ b/Code/Framework/AzCore/AzCore/Asset/AssetTypeInfoBus.h
@@ -57,7 +57,13 @@ namespace AZ
//! Determines if a component can be created from the asset type
//! This will be called before attempting to create a component from an asset (drag&drop, etc)
//! You can use this to filter by subIds or do your own validation here if needed
- virtual bool CanCreateComponent(const AZ::Data::AssetId& /*assetId*/) const { return true; }
+ virtual bool CanCreateComponent([[maybe_unused]] const AZ::Data::AssetId& assetId) const { return true; }
+
+ //! Determines if other products conflict with the given one when multiple are generated from a source asset.
+ //! This will be called before attempting to create a component from an asset (drag&drop, etc)
+ //! You can use this to filter by conflicting product types or in case you want to skip for UX reasons.
+ //! @param[in] productAssetTypes Asset types of all generated products, including the one for our given type in this bus.
+ virtual bool HasConflictingProducts([[maybe_unused]] const AZStd::vector& productAssetTypes) const { return false; }
};
using AssetTypeInfoBus = AZ::EBus;
diff --git a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h
index 479828c9d3..d51fdc36f3 100644
--- a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h
+++ b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StorageDrive_Windows.h
@@ -44,8 +44,7 @@ namespace AZ::IO
//! make adjustments. For the most optimal performance align read buffers to the physicalSectorSize.
u8 m_enableUnbufferedReads : 1;
//! Globally enable file sharing. This allows files to used outside AZ::IO::Streamer, including other applications
- //! while in use by AZ::IO::Streamer. File sharing can negatively impact performance and is recommended for
- //! development only.
+ //! while in use by AZ::IO::Streamer.
u8 m_enableSharing : 1;
//! If true, only information that's explicitly requested or issues are reported. If false, status information
//! such as when drives are created and destroyed is reported as well.
diff --git a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StreamerConfiguration_Windows.cpp b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StreamerConfiguration_Windows.cpp
index 5a19bc4f54..28831a00b0 100644
--- a/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StreamerConfiguration_Windows.cpp
+++ b/Code/Framework/AzCore/Platform/Windows/AzCore/IO/Streamer/StreamerConfiguration_Windows.cpp
@@ -312,7 +312,7 @@ namespace AZ::IO
{
if (reportHardware)
{
- AZ_Printf("Streamer", "Skipping drive '%s' because to no paths make use of it.\n", driveIt);
+ AZ_Printf("Streamer", "Skipping drive '%s' because no paths make use of it.\n", driveIt);
}
while (*driveIt++);
continue;
diff --git a/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp b/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp
index bab531deb6..ecf6873052 100644
--- a/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp
+++ b/Code/Framework/AzCore/Tests/Platform/Windows/Tests/IO/Streamer/StorageDriveTests_Windows.cpp
@@ -46,6 +46,7 @@ namespace AZ::IO
options.m_hasSeekPenalty = HasSeekPenalty;
options.m_enableUnbufferedReads = TestEnableUnbufferReads;
options.m_enableSharing = TestEnableSharedReads;
+ options.m_minimalReporting = true;
return StorageDriveWin({ "c:/" }, TestMaxFileHandles, TestMaxMetaDataEntries, TestPhysicalSectorSize,
TestLogicalSectorSize, TestMaxIOChannels, TestOverCommit, options);
@@ -151,6 +152,7 @@ namespace AZ::IO
m_configurationOptions.m_hasSeekPenalty = HasSeekPenalty;
m_configurationOptions.m_enableUnbufferedReads = TestEnableUnbufferReads;
m_configurationOptions.m_enableSharing = TestEnableSharedReads;
+ m_configurationOptions.m_minimalReporting = true;
m_storageDriveWin = AZStd::make_shared(AZStd::vector{drive}, TestMaxFileHandles,
TestMaxMetaDataEntries, TestPhysicalSectorSize, TestLogicalSectorSize, TestMaxIOChannels, overCommit, m_configurationOptions);
@@ -1148,3 +1150,142 @@ namespace AZ::IO
azfree(buffers[numRequests - 1]);
}
} // namespace AZ::IO
+
+#if defined(HAVE_BENCHMARK)
+
+#include
+
+namespace Benchmark
+{
+ class StorageDriveWindowsFixture : public benchmark::Fixture
+ {
+ public:
+ constexpr static char* TestFileName = "StreamerBenchmark.bin";
+ constexpr static size_t FileSize = 64_mib;
+
+ void SetupStreamer(bool enableFileSharing)
+ {
+ using namespace AZ::IO;
+
+ m_fileIO = new UnitTest::TestFileIOBase();
+ m_previousFileIO = AZ::IO::FileIOBase::GetInstance();
+ AZ::IO::FileIOBase::SetInstance(nullptr);
+ AZ::IO::FileIOBase::SetInstance(m_fileIO);
+
+ SystemFile file;
+ file.Open(TestFileName, SystemFile::OpenMode::SF_OPEN_CREATE | SystemFile::OpenMode::SF_OPEN_READ_WRITE);
+ AZStd::unique_ptr buffer(new char[FileSize]);
+ ::memset(buffer.get(), 'c', FileSize);
+
+ file.Write(buffer.get(), FileSize);
+ file.Close();
+
+ AZStd::optional absolutePath = AZ::Utils::ConvertToAbsolutePath(TestFileName);
+ if (absolutePath.has_value())
+ {
+ AZStd::string drive;
+ AZ::StringFunc::Path::GetDrive(absolutePath->c_str(), drive);
+
+ m_absolutePath = *absolutePath;
+
+ StorageDriveWin::ConstructionOptions options;
+ options.m_hasSeekPenalty = false;
+ options.m_enableUnbufferedReads = true; // Leave this on otherwise repeated loads will be using the Windows cache instead.
+ options.m_enableSharing = enableFileSharing;
+ options.m_minimalReporting = true;
+ AZStd::shared_ptr storageDriveWin =
+ AZStd::make_shared(AZStd::vector{ drive }, 32, 32, 4_kib, 512, 8, 0, options);
+
+ AZStd::unique_ptr stack = AZStd::make_unique(AZStd::move(storageDriveWin));
+ m_streamer = aznew Streamer(AZStd::thread_desc{}, AZStd::move(stack));
+ }
+ }
+
+ void TearDown([[maybe_unused]] const ::benchmark::State& state) override
+ {
+ using namespace AZ::IO;
+
+ AZStd::string temp;
+ m_absolutePath.swap(temp);
+
+ delete m_streamer;
+
+ SystemFile::Delete(TestFileName);
+
+ AZ::IO::FileIOBase::SetInstance(nullptr);
+ AZ::IO::FileIOBase::SetInstance(m_previousFileIO);
+ delete m_fileIO;
+ }
+
+ void RepeatedlyReadFile(benchmark::State& state)
+ {
+ using namespace AZ::IO;
+ using namespace AZStd::chrono;
+
+ AZStd::unique_ptr buffer(new char[FileSize]);
+
+ for (auto _ : state)
+ {
+ AZStd::binary_semaphore waitForReads;
+ AZStd::atomic end;
+ auto callback = [&end, &waitForReads]([[maybe_unused]] FileRequestHandle request)
+ {
+ benchmark::DoNotOptimize(end = high_resolution_clock::now());
+ waitForReads.release();
+ };
+
+ FileRequestPtr request = m_streamer->Read(m_absolutePath, buffer.get(), state.range(0), state.range(0));
+ m_streamer->SetRequestCompleteCallback(request, callback);
+
+ system_clock::time_point start;
+ benchmark::DoNotOptimize(start = high_resolution_clock::now());
+ m_streamer->QueueRequest(request);
+
+ waitForReads.try_acquire_for(AZStd::chrono::seconds(5));
+ auto durationInSeconds = duration_cast>(end.load() - start);
+
+ state.SetIterationTime(durationInSeconds.count());
+
+ m_streamer->QueueRequest(m_streamer->FlushCaches());
+ }
+ }
+
+ AZStd::string m_absolutePath;
+ AZ::IO::Streamer* m_streamer{};
+ AZ::IO::FileIOBase* m_previousFileIO{};
+ UnitTest::TestFileIOBase* m_fileIO{};
+ };
+
+ BENCHMARK_DEFINE_F(StorageDriveWindowsFixture, ReadsBaseline)(benchmark::State& state)
+ {
+ constexpr bool EnableFileSharing = false;
+ SetupStreamer(EnableFileSharing);
+ RepeatedlyReadFile(state);
+ }
+
+ BENCHMARK_DEFINE_F(StorageDriveWindowsFixture, ReadsWithFileReadSharingEnabled)(benchmark::State& state)
+ {
+ using namespace AZ::IO;
+
+ constexpr bool EnableFileSharing = true;
+ SetupStreamer(EnableFileSharing);
+ RepeatedlyReadFile(state);
+ }
+
+ // For these benchmarks the CPU stat doesn't provide useful information because it uses GetThreadTimes on Window but since the main
+ // thread is mostly sleeping while waiting for the read on the Streamer thread to complete this will report values (close to) zero.
+
+ BENCHMARK_REGISTER_F(StorageDriveWindowsFixture, ReadsBaseline)
+ ->RangeMultiplier(8)
+ ->Range(1024, 64_mib)
+ ->UseManualTime()
+ ->Unit(benchmark::kMillisecond);
+
+ BENCHMARK_REGISTER_F(StorageDriveWindowsFixture, ReadsWithFileReadSharingEnabled)
+ ->RangeMultiplier(8)
+ ->Range(1024, 64_mib)
+ ->UseManualTime()
+ ->Unit(benchmark::kMillisecond);
+
+} // namespace Benchmark
+#endif // HAVE_BENCHMARK
diff --git a/Code/Framework/AzCore/Tests/Serialization/Json/JsonSerializerConformityTests.h b/Code/Framework/AzCore/Tests/Serialization/Json/JsonSerializerConformityTests.h
index 9d7e58dd36..a46d8ad9d4 100644
--- a/Code/Framework/AzCore/Tests/Serialization/Json/JsonSerializerConformityTests.h
+++ b/Code/Framework/AzCore/Tests/Serialization/Json/JsonSerializerConformityTests.h
@@ -1206,7 +1206,7 @@ namespace JsonSerializationTests
if (this->m_features.m_enableInitializationTest)
{
auto instance = this->m_description.CreateDefaultInstance();
- typename TypeParam::Type compare = typename TypeParam::Type{};
+ AZStd::remove_cvref_t compare;
if (!this->m_description.AreEqual(*instance, compare))
{
auto serializer = this->m_description.CreateSerializer();
diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h
index 8236683163..d50e239f9a 100644
--- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h
+++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesInterface.h
@@ -198,11 +198,13 @@ namespace AzFramework
AZ::SerializeContext* m_serializeContext{ nullptr };
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
- //! Entity references are resolved by referring to the last entity spawned from a template entity in the spawnable. If this
- //! is set to false entities from previous spawn calls are not taken into account. If set to true entity references may be
- //! resolved to a previously spawned entity. A lookup table has to be constructed when true, which may negatively impact
- //! performance, especially if a large number of entities are present on a ticket.
- bool m_referencePreviouslySpawnedEntities{ false };
+ //! Entity references are resolved by referring to the most recent entity spawned from a template entity in the spawnable.
+ //! If the entity referred to hasn't been spawned yet, the reference will be resolved to the first one that *will* be spawned.
+ //! If this flag is set to "true", the id mappings will persist across SpawnEntites calls, and the entity references will resolve
+ //! correctly across them.
+ //! When "false", the entity id mappings will be reset on this call, so entity references will only work within this call, or
+ //! potentially with any subsequent SpawnEntities call where the flag is true once again.
+ bool m_referencePreviouslySpawnedEntities{ true };
};
struct DespawnAllEntitiesOptionalArgs final
diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp
index d6d005a944..8cbc60d94e 100644
--- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp
+++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp
@@ -250,10 +250,47 @@ namespace AzFramework
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
{
- return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs(
+ // If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it.
+ constexpr bool allowDuplicateIds = false;
+
+ return AZ::IdUtils::Remapper::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext);
}
+ void SpawnableEntitiesManager::InitializeEntityIdMappings(
+ const Spawnable::EntityList& entities, EntityIdMap& idMap, AZStd::unordered_set& previouslySpawned)
+ {
+ // Make sure we don't have any previous data lingering around.
+ idMap.clear();
+ previouslySpawned.clear();
+
+ idMap.reserve(entities.size());
+ previouslySpawned.reserve(entities.size());
+
+ for (auto& entity : entities)
+ {
+ idMap.emplace(entity->GetId(), AZ::Entity::MakeId());
+ }
+ }
+
+ void SpawnableEntitiesManager::RefreshEntityIdMapping(
+ const AZ::EntityId& entityId, EntityIdMap& idMap, AZStd::unordered_set& previouslySpawned)
+ {
+ if (previouslySpawned.contains(entityId))
+ {
+ // This entity has already been spawned at least once before, so we need to generate a new id for it and
+ // preserve the new id to fix up any future entity references to this entity.
+ idMap[entityId] = AZ::Entity::MakeId();
+ }
+ else
+ {
+ // This entity hasn't been spawned yet, so use the first id we've already generated for this entity and mark
+ // it as spawned so we know not to reuse this id next time.
+ previouslySpawned.emplace(entityId);
+ }
+ }
+
+
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
@@ -269,18 +306,24 @@ namespace AzFramework
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = entitiesToSpawn.size();
- // Map keeps track of ids from template (spawnable) to clone (instance)
- // Allowing patch ups of fields referring to entityIds outside of a given entity
- EntityIdMap templateToCloneEntityIdMap;
-
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
- templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
+
+ // Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
+ // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
+ // We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
+ // in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
+ // of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
+ // previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
+ InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{
- AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], templateToCloneEntityIdMap, *request.m_serializeContext);
+ // If this entity has previously been spawned, give it a new id in the reference map
+ RefreshEntityIdMapping(entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
+
+ AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.emplace_back(clone);
@@ -337,21 +380,17 @@ namespace AzFramework
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
- // Reconstruct the template to entity mapping.
- EntityIdMap templateToCloneEntityIdMap;
- if (!request.m_referencePreviouslySpawnedEntities)
+ if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
{
- templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
- }
- else
- {
- templateToCloneEntityIdMap.reserve(spawnedEntitiesInitialCount + entitiesToSpawnSize);
- SpawnableConstIndexEntityContainerView indexEntityView(
- spawnedEntities.begin(), spawnedEntityIndices.begin(), spawnedEntities.size());
- for (auto& entry : indexEntityView)
- {
- templateToCloneEntityIdMap.insert_or_assign(entitiesToSpawn[entry.GetIndex()]->GetId(), entry.GetEntity()->GetId());
- }
+ // This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring
+ // to entityIds outside of a given entity.
+ // We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
+ // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
+ // By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
+ // that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
+ // (or SpawnAllEntities) call.
+ // However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
+ InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
}
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
@@ -361,7 +400,12 @@ namespace AzFramework
{
if (index < entitiesToSpawn.size())
{
- AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[index], templateToCloneEntityIdMap, *request.m_serializeContext);
+ // If this entity has previously been spawned, give it a new id in the reference map
+ RefreshEntityIdMapping(
+ entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
+
+ AZ::Entity* clone =
+ CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.push_back(clone);
@@ -451,9 +495,11 @@ namespace AzFramework
ticket.m_spawnedEntities.clear();
const Spawnable::EntityList& entities = request.m_spawnable->GetEntities();
- // Map keeps track of ids from template (spawnable) to clone (instance)
- // Allowing patch ups of fields referring to entityIds outside of a given entity
- EntityIdMap templateToCloneEntityIdMap;
+ // Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
+ // any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
+ // This map is intentionally cleared out and regenerated here to ensure that we're starting fresh with mappings that
+ // match the new set of template entities getting spawned.
+ InitializeEntityIdMappings(entities, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
if (ticket.m_loadAll)
{
@@ -461,11 +507,13 @@ namespace AzFramework
// to spawn every entity, simply start over.
ticket.m_spawnedEntityIndices.clear();
size_t entitiesToSpawnSize = entities.size();
- templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{
- AZ::Entity* clone = CloneSingleEntity(*entities[i], templateToCloneEntityIdMap, *request.m_serializeContext);
+ // If this entity has previously been spawned, give it a new id in the reference map
+ RefreshEntityIdMapping(entities[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
+
+ AZ::Entity* clone = CloneSingleEntity(*entities[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
ticket.m_spawnedEntities.push_back(clone);
@@ -475,7 +523,7 @@ namespace AzFramework
else
{
size_t entitiesSize = entities.size();
- templateToCloneEntityIdMap.reserve(entitiesSize);
+
for (size_t index : ticket.m_spawnedEntityIndices)
{
// It's possible for the new spawnable to have a different number of entities, so guard against this.
@@ -483,7 +531,10 @@ namespace AzFramework
// detected and will result in the incorrect entities being spawned.
if (index < entitiesSize)
{
- AZ::Entity* clone = CloneSingleEntity(*entities[index], templateToCloneEntityIdMap, *request.m_serializeContext);
+ // If this entity has previously been spawned, give it a new id in the reference map
+ RefreshEntityIdMapping(entities[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
+
+ AZ::Entity* clone = CloneSingleEntity(*entities[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
ticket.m_spawnedEntities.push_back(clone);
}
diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h
index 5f659182d3..0c3f1c9ef5 100644
--- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h
+++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.h
@@ -85,6 +85,22 @@ namespace AzFramework
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
static constexpr uint32_t Processing = AZStd::numeric_limits::max();
+ //! Map of template entity ids to their associated instance ids.
+ //! Tickets can be used to spawn the same template entities multiple times, in any order, across multiple calls.
+ //! Since template entities can reference other entities, this map is used to fix up those references across calls
+ //! using the following policy:
+ //! - Entities referencing an entity that hasn't been spawned yet will get a reference to the id that *will* be used
+ //! the first time that entity will be spawned. The reference will be invalid until that entity is spawned, but
+ //! will be valid if/when it gets spawned.
+ //! - Entities referencing an entity that *has* been spawned will get a reference to the id that was *last* used to
+ //! spawn the entity.
+ //! Note that this implies a certain level of non-determinism when spawning across calls, because the entity references
+ //! will be based on the order in which the SpawnEntity calls occur, which can be affected by things like priority.
+ EntityIdMap m_entityIdReferenceMap;
+ //! For this to work, we also need to keep track of whether or not each entity has been spawned at least once, so we know
+ //! whether or not to replace the id in the map when spawning a new instance of that entity.
+ AZStd::unordered_set m_previouslySpawned;
+
AZStd::vector m_spawnedEntities;
AZStd::vector m_spawnedEntityIndices;
AZ::Data::Asset m_spawnable;
@@ -194,6 +210,15 @@ namespace AzFramework
bool ProcessRequest(BarrierCommand& request);
bool ProcessRequest(DestroyTicketCommand& request);
+ //! Generate a base set of original-to-new entity ID mappings to use during spawning.
+ //! Since Entity references get fixed up on an entity-by-entity basis while spawning, it's important to have the complete
+ //! set of new IDs available right at the start. This way, entities that refer to other entities that haven't spawned yet
+ //! will still get their references remapped correctly.
+ void InitializeEntityIdMappings(
+ const Spawnable::EntityList& entities, EntityIdMap& idMap, AZStd::unordered_set& previouslySpawned);
+ void RefreshEntityIdMapping(
+ const AZ::EntityId& entityId, EntityIdMap& idMap, AZStd::unordered_set& previouslySpawned);
+
Queue m_highPriorityQueue;
Queue m_regularPriorityQueue;
diff --git a/Code/Framework/AzFramework/Platform/Mac/AzFramework/Windowing/NativeWindow_Mac.mm b/Code/Framework/AzFramework/Platform/Mac/AzFramework/Windowing/NativeWindow_Mac.mm
index 3eba9831b4..8b911c90e9 100644
--- a/Code/Framework/AzFramework/Platform/Mac/AzFramework/Windowing/NativeWindow_Mac.mm
+++ b/Code/Framework/AzFramework/Platform/Mac/AzFramework/Windowing/NativeWindow_Mac.mm
@@ -75,6 +75,8 @@ namespace AzFramework
// Add a fullscreen button in the upper right of the title bar.
[m_nativeWindow setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
+ m_nativeWindow.tabbingMode = NSWindowTabbingModeDisallowed;
+
// Make the window active
[m_nativeWindow makeKeyAndOrderFront:nil];
m_nativeWindow.title = m_windowTitle;
diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp
index 0f58f06420..0d770d2d8e 100644
--- a/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp
+++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/Style.cpp
@@ -493,7 +493,30 @@ namespace AzQtComponents
}
}
break;
- }
+ case CE_MenuItem:
+ {
+ const QMenu* menu = qobject_cast(widget);
+ QAction* action = menu->activeAction();
+ if (action)
+ {
+ QMenu* subMenu = action->menu();
+ if (subMenu)
+ {
+ QVariant noHover = subMenu->property("noHover");
+ if (noHover.isValid() && noHover.toBool())
+ {
+ // First draw as standard to get the correct hover background for the complete control.
+ QProxyStyle::drawControl(element, option, painter, widget);
+ // Now draw the icon as non-hovered so control behaves as designed.
+ QStyleOptionMenuItem myOpt = *qstyleoption_cast(option);
+ myOpt.state &= ~QStyle::State_Selected;
+ return QProxyStyle::drawControl(element, &myOpt, painter, widget);
+ }
+ }
+ }
+ }
+ break;
+ }
return QProxyStyle::drawControl(element, option, painter, widget);
}
diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Menu/helpers.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Menu/helpers.svg
new file mode 100644
index 0000000000..e782a7066a
--- /dev/null
+++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Menu/helpers.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Menu/menu.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Menu/menu.svg
new file mode 100644
index 0000000000..e97da32e09
--- /dev/null
+++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Menu/menu.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg
index dfd21d157f..6f5608c092 100644
--- a/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg
+++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Notifications/link.svg
@@ -1,4 +1,4 @@
diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc
index cc66558367..74610dca90 100644
--- a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc
+++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc
@@ -28,5 +28,7 @@
Menu/script_canvas_editor.svg
Menu/trackview_editor.svg
Menu/ui_editor.svg
+ Menu/menu.svg
+ Menu/helpers.svg
diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp
index 336b56653b..1172fc7162 100644
--- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp
+++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp
@@ -524,8 +524,6 @@ namespace AzToolsFramework
rootSpawnableIndex = m_playInEditorData.m_assets.size();
}
- LoadReferencedAssets(product.GetReferencedAssets());
-
AZ::Data::AssetInfo info;
info.m_assetId = product.GetAsset().GetId();
info.m_assetType = product.GetAssetType();
@@ -534,6 +532,19 @@ namespace AzToolsFramework
AZ::Data::AssetCatalogRequestBus::Broadcast(
&AZ::Data::AssetCatalogRequestBus::Events::RegisterAsset, info.m_assetId, info);
m_playInEditorData.m_assets.emplace_back(product.ReleaseAsset().release(), AZ::Data::AssetLoadBehavior::Default);
+
+ // Ensure the product asset is registered with the AssetManager
+ // Hold on to the returned asset to keep ref count alive until we assign it the latest data
+ AZ::Data::Asset asset =
+ AZ::Data::AssetManager::Instance().FindOrCreateAsset(info.m_assetId, info.m_assetType, AZ::Data::AssetLoadBehavior::Default);
+
+ // Update the asset registered in the AssetManager with the data of our product from the Prefab Processor
+ AZ::Data::AssetManager::Instance().AssignAssetData(m_playInEditorData.m_assets.back());
+ }
+
+ for (auto& product : context.GetProcessedObjects())
+ {
+ LoadReferencedAssets(product.GetReferencedAssets());
}
// make sure that PRE_NOTIFY assets get their notify before we activate, so that we can preserve the order of
diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp
index 3d02e797cc..a5cd50b36e 100644
--- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp
+++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp
@@ -922,8 +922,8 @@ namespace AzToolsFramework
return AZ::Failure(AZStd::string("Failed to duplicate : Couldn't get a valid owning instance for the common root entity of the entities provided."));
}
- // If the first entity id is a container entity id, then we need to mark its parent as the common owning instance because you
- // cannot duplicate an instance from itself.
+ // If the first entity id is a container entity id, then we need to mark its parent as the common owning instance
+ // This is because containers, despite representing the nested instance in the parent, are owned by the child.
if (commonOwningInstance->get().GetContainerEntityId() == firstEntityIdToDuplicate)
{
commonOwningInstance = commonOwningInstance->get().GetParentInstance();
@@ -967,17 +967,18 @@ namespace AzToolsFramework
// Duplicate any nested entities and instances as requested
AZStd::unordered_map newInstanceAliasToOldInstanceMap;
+ AZStd::unordered_map duplicateEntityAliasMap;
DuplicateNestedEntitiesInInstance(commonOwningInstance->get(),
- entities, instanceDomAfter, duplicatedEntityAndInstanceIds);
- DuplicateNestedInstancesInInstance(commonOwningInstance->get(),
- instances, instanceDomAfter, duplicatedEntityAndInstanceIds,
- newInstanceAliasToOldInstanceMap);
+ entities, instanceDomAfter, duplicatedEntityAndInstanceIds, duplicateEntityAliasMap);
PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity/Instance duplication");
command->SetParent(undoBatch.GetUndoBatch());
command->Capture(instanceDomBefore, instanceDomAfter, commonOwningInstance->get().GetTemplateId());
command->Redo();
+ DuplicateNestedInstancesInInstance(commonOwningInstance->get(),
+ instances, instanceDomAfter, duplicatedEntityAndInstanceIds, newInstanceAliasToOldInstanceMap);
+
// Create links for our duplicated instances (if any were duplicated)
for (auto [newInstanceAlias, oldInstance] : newInstanceAliasToOldInstanceMap)
{
@@ -995,8 +996,35 @@ namespace AzToolsFramework
PrefabDom linkPatchesCopy;
linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator());
- m_prefabSystemComponentInterface->CreateLink(
- commonOwningInstance->get().GetTemplateId(), oldInstance->GetTemplateId(), newInstanceAlias, linkPatchesCopy);
+ // If the instance was duplicated as part of an ancestor's nested hierarchy, the container's parent patch
+ // will need to be refreshed to point to the new duplicated parent entity
+ auto oldInstanceContainerEntityId = oldInstance->GetContainerEntityId();
+ AZ_Assert(oldInstanceContainerEntityId.IsValid(), "Instance returned invalid Container Entity Id");
+
+ AZ::EntityId previousParentEntityId;
+ AZ::TransformBus::EventResult(previousParentEntityId, oldInstanceContainerEntityId, &AZ::TransformBus::Events::GetParentId);
+
+ if (previousParentEntityId.IsValid() && AZStd::find(duplicatedEntityAndInstanceIds.begin(), duplicatedEntityAndInstanceIds.end(), previousParentEntityId))
+ {
+ auto oldParentAlias = commonOwningInstance->get().GetEntityAlias(previousParentEntityId);
+ if (oldParentAlias.has_value() && duplicateEntityAliasMap.contains(oldParentAlias->get()))
+ {
+ // Get the dom into a QString for search/replace purposes
+ rapidjson::StringBuffer buffer;
+ rapidjson::Writer writer(buffer);
+ linkPatchesCopy.Accept(writer);
+
+ QString linkPatchesString(buffer.GetString());
+
+ ReplaceOldAliases(linkPatchesString, oldParentAlias->get(), duplicateEntityAliasMap[oldParentAlias->get()]);
+
+ linkPatchesCopy.Parse(linkPatchesString.toUtf8().constData());
+ }
+ }
+
+ PrefabUndoHelpers::CreateLink(
+ oldInstance->GetTemplateId(), commonOwningInstance->get().GetTemplateId(),
+ AZStd::move(linkPatchesCopy), newInstanceAlias, undoBatch.GetUndoBatch());
}
// Select the duplicated entities/instances
@@ -1507,14 +1535,13 @@ namespace AzToolsFramework
void PrefabPublicHandler::DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance,
const AZStd::vector& entities, PrefabDom& domToAddDuplicatedEntitiesUnder,
- EntityIdList& duplicatedEntityIds)
+ EntityIdList& duplicatedEntityIds, AZStd::unordered_map& oldAliasToNewAliasMap)
{
if (entities.empty())
{
return;
}
- AZStd::unordered_map oldAliasToNewAliasMap;
AZStd::unordered_map aliasToEntityDomMap;
for (AZ::Entity* entity : entities)
diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h
index 65e1391722..fc5906c80e 100644
--- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h
+++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h
@@ -87,7 +87,7 @@ namespace AzToolsFramework
*/
void DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance,
const AZStd::vector& entities, PrefabDom& domToAddDuplicatedEntitiesUnder,
- EntityIdList& duplicatedEntityIds);
+ EntityIdList& duplicatedEntityIds, AZStd::unordered_map& oldAliasToNewAliasMap);
/**
* Duplicate a list of instances owned by a common owning instance by directly
* copying/modifying their entries in the instance DOM
diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp
index f72de82f36..4cd18cbc91 100644
--- a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp
+++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
@@ -57,6 +58,13 @@ namespace AzToolsFramework
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly);
}
}
+
+ if (auto behaviorContext = azrtti_cast(context))
+ {
+ behaviorContext->ConstantProperty("EditorNonUniformScaleComponentTypeId", BehaviorConstant(EditorNonUniformScaleComponent::RTTI_Type()))
+ ->Attribute(AZ::Script::Attributes::Module, "editor")
+ ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
+ }
}
void EditorNonUniformScaleComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent)
diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h
index 85250f2a32..b4d0342c44 100644
--- a/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h
+++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h
@@ -210,6 +210,19 @@ namespace AzToolsFramework
//! Type to inherit to implement ViewportInteractionRequests.
using ViewportInteractionRequestBus = AZ::EBus;
+ //! An interface to notify when changes to viewport settings have happened.
+ class ViewportSettingNotifications
+ {
+ public:
+ virtual void OnGridSnappingChanged([[maybe_unused]] bool enabled) {}
+ virtual void OnDrawHelpersChanged([[maybe_unused]] bool enabled) {}
+
+ protected:
+ ViewportSettingNotifications() = default;
+ };
+
+ using ViewportSettingsNotificationBus = AZ::EBus;
+
//! Requests to freeze the Viewport Input
//! Added to prevent a bug with the legacy CryEngine Viewport code that would
//! keep doing raycast tests even when no level is loaded, causing a crash.
diff --git a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp
index f0614aa13b..462cf949b0 100644
--- a/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp
+++ b/Code/Framework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp
@@ -32,6 +32,33 @@ namespace UnitTest
}
};
+ // Test component that has a reference to a different entity for use in validating per-instance entity id fixups.
+ class ComponentWithEntityReference : public AZ::Component
+ {
+ public:
+ AZ_COMPONENT(ComponentWithEntityReference, "{CF5FDE59-86E5-40B6-9272-BBC1C4AFD061}");
+
+ void Activate() override
+ {
+ }
+
+ void Deactivate() override
+ {
+ }
+
+ static void Reflect(AZ::ReflectContext* reflection)
+ {
+ if (auto* serializeContext = azrtti_cast(reflection))
+ {
+ serializeContext->Class()
+ ->Field("EntityReference", &ComponentWithEntityReference::m_entityReference)
+ ;
+ }
+ }
+
+ AZ::EntityId m_entityReference;
+ };
+
class SpawnableEntitiesManagerTest : public AllocatorsFixture
{
public:
@@ -42,6 +69,8 @@ namespace UnitTest
m_application = new TestApplication();
AZ::ComponentApplication::Descriptor descriptor;
m_application->Start(descriptor);
+ m_application->RegisterComponentDescriptor(ComponentWithEntityReference::CreateDescriptor());
+
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
// in the unit tests.
@@ -80,6 +109,7 @@ namespace UnitTest
void FillSpawnable(size_t numElements)
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
+ entities.clear();
entities.reserve(numElements);
for (size_t i=0; iGetEntities();
+ size_t numElements = entities.size();
+ for (size_t i = 0; i < numElements; ++i)
+ {
+ AZStd::unique_ptr& entity = entities[i];
+ auto component = entity->CreateComponent();
+ switch (refScheme)
+ {
+ case EntityReferenceScheme::AllReferenceFirst :
+ component->m_entityReference = entities[0]->GetId();
+ break;
+ case EntityReferenceScheme::AllReferenceLast:
+ component->m_entityReference = entities[numElements - 1]->GetId();
+ break;
+ case EntityReferenceScheme::AllReferenceThemselves:
+ component->m_entityReference = entities[i]->GetId();
+ break;
+ case EntityReferenceScheme::AllReferenceNextCircular:
+ component->m_entityReference = entities[(i + 1) % numElements]->GetId();
+ break;
+ case EntityReferenceScheme::AllReferencePreviousCircular:
+ component->m_entityReference = entities[(i + numElements - 1) % numElements]->GetId();
+ break;
+ }
+ }
+ }
+
+ // Verify that the entity references are pointing to the correct other entities within the same spawn batch.
+ // A "spawn batch" is the set of entities produced for each SpawnAllEntities command.
+ void ValidateEntityReferences(
+ EntityReferenceScheme refScheme, size_t entitiesPerBatch, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ size_t numElements = entities.size();
+
+ for (size_t i = 0; i < numElements; ++i)
+ {
+ // Calculate the element offset that's the start of each batch of entities spawned.
+ size_t curSpawnBatch = i / entitiesPerBatch;
+ size_t curBatchOffset = curSpawnBatch * entitiesPerBatch;
+ size_t curBatchIndex = i - curBatchOffset;
+
+ const AZ::Entity* const entity = *(entities.begin() + i);
+
+ auto component = entity->FindComponent();
+ ASSERT_NE(nullptr, component);
+ AZ::EntityId comparisonId;
+ // Ids should be local to a batch, so each of these will be compared within a batch of entities, not globally across
+ // the entire set.
+ switch (refScheme)
+ {
+ case EntityReferenceScheme::AllReferenceFirst:
+ // Compare against the first entity in each batch
+ comparisonId = (*(entities.begin() + curBatchOffset))->GetId();
+ break;
+ case EntityReferenceScheme::AllReferenceLast:
+ // Compare against the last entity in each batch
+ comparisonId = (*(entities.begin() + curBatchOffset + (entitiesPerBatch - 1)))->GetId();
+ break;
+ case EntityReferenceScheme::AllReferenceThemselves:
+ // Compare against itself
+ comparisonId = entity->GetId();
+ break;
+ case EntityReferenceScheme::AllReferenceNextCircular:
+ // Compare against the next entity in each batch, looping around so that the last entity in the batch should refer
+ // to the first entity in the batch.
+ comparisonId = (*(entities.begin() + curBatchOffset + ((curBatchIndex + 1) % entitiesPerBatch)))->GetId();
+ break;
+ case EntityReferenceScheme::AllReferencePreviousCircular:
+ // Compare against the previous entity in each batch, looping around so that the first entity in the batch should refer
+ // to the last entity in the batch.
+ comparisonId = (*(entities.begin() + curBatchOffset + ((curBatchIndex + numElements - 1) % entitiesPerBatch)))->GetId();
+ break;
+ }
+ EXPECT_EQ(comparisonId, component->m_entityReference);
+ }
+ };
+
protected:
AZ::Data::Asset* m_spawnableAsset { nullptr };
AzFramework::SpawnableEntitiesManager* m_manager { nullptr };
@@ -185,6 +303,73 @@ namespace UnitTest
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
+ TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllEntitiesReferenceOtherEntities_EntityIdsAreMappedCorrectly)
+ {
+ // This tests that entity id references get mapped correctly in a SpawnAllEntities call whether they're forward referencing
+ // in the list, backwards referencing, or self-referencing. The circular tests are to ensure the implementation works regardless
+ // of entity ordering.
+ for (EntityReferenceScheme refScheme : {
+ EntityReferenceScheme::AllReferenceFirst, EntityReferenceScheme::AllReferenceLast,
+ EntityReferenceScheme::AllReferenceThemselves, EntityReferenceScheme::AllReferenceNextCircular,
+ EntityReferenceScheme::AllReferencePreviousCircular })
+ {
+ constexpr size_t NumEntities = 4;
+ FillSpawnable(NumEntities);
+ CreateEntityReferences(refScheme);
+
+ auto callback = [this, refScheme, NumEntities]
+ (AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ ValidateEntityReferences(refScheme, NumEntities, entities);
+ };
+ AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
+ optionalArgs.m_completionCallback = AZStd::move(callback);
+ m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
+ m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
+ }
+ }
+
+ TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllEntitiesReferenceOtherEntities_EntityIdsOnlyReferWithinASingleCall)
+ {
+ // This tests that entity id references get mapped correctly with multiple SpawnAllEntities calls. Each call should only map
+ // the entities to other entities within the same call, regardless of forward or backward mapping.
+ // For example, suppose entities 1, 2, and 3 refer to 4. In the first SpawnAllEntities call, entities 1-3 will refer to 4.
+ // In the second SpawnAllEntities call, entities 1-3 will refer to the second 4, not the previously-spawned 4.
+ for (EntityReferenceScheme refScheme :
+ { EntityReferenceScheme::AllReferenceFirst, EntityReferenceScheme::AllReferenceLast,
+ EntityReferenceScheme::AllReferenceThemselves, EntityReferenceScheme::AllReferenceNextCircular,
+ EntityReferenceScheme::AllReferencePreviousCircular
+ })
+ {
+ // Make sure we start with a fresh ticket each time, or else each iteration through this loop would continue to build up
+ // more and more entities.
+ delete m_ticket;
+ m_ticket = new AzFramework::EntitySpawnTicket(*m_spawnableAsset);
+
+ constexpr size_t NumEntities = 4;
+ FillSpawnable(NumEntities);
+ CreateEntityReferences(refScheme);
+
+ auto callback = [this, refScheme, NumEntities]
+ (AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ ValidateEntityReferences(refScheme, NumEntities, entities);
+ };
+
+ // Spawn twice.
+ constexpr size_t NumSpawnAllCalls = 2;
+ for (int spawns = 0; spawns < NumSpawnAllCalls; spawns++)
+ {
+ AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
+ optionalArgs.m_completionCallback = AZStd::move(callback);
+ m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
+ }
+
+ m_manager->ListEntities(*m_ticket, callback);
+ m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
+ }
+ }
+
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
{
@@ -363,6 +548,180 @@ namespace UnitTest
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
+ TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_ForwardReferencesWorkInSingleCall)
+ {
+ constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceNextCircular;
+ constexpr size_t NumEntities = 4;
+ FillSpawnable(NumEntities);
+ CreateEntityReferences(refScheme);
+
+ auto callback =
+ [this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ ValidateEntityReferences(refScheme, NumEntities, entities);
+ };
+
+ // Verify that by default, entities that refer to other entities that haven't been spawned yet have the correct references
+ // when the spawning all occurs in the same call
+ m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 });
+ m_manager->ListEntities(*m_ticket, callback);
+ m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
+ }
+
+ TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_ForwardReferencesWorkAcrossCalls)
+ {
+ constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceNextCircular;
+ constexpr size_t NumEntities = 4;
+ FillSpawnable(NumEntities);
+ CreateEntityReferences(refScheme);
+
+ auto callback =
+ [this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ ValidateEntityReferences(refScheme, NumEntities, entities);
+ };
+
+ // Verify that by default, entities that refer to other entities that haven't been spawned yet have the correct references
+ // even when the spawning is across multiple calls
+ m_manager->SpawnEntities(*m_ticket, { 0 });
+ m_manager->SpawnEntities(*m_ticket, { 1 });
+ m_manager->SpawnEntities(*m_ticket, { 2 });
+ m_manager->SpawnEntities(*m_ticket, { 3 });
+ m_manager->ListEntities(*m_ticket, callback);
+ m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
+ }
+
+ TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_ReferencesPointToFirstOrLatest)
+ {
+ // With SpawnEntities, entity references should either refer to the first entity that *will* be spawned, or the last entity
+ // that *has* been spawned. This test will create entities 0 1 2 3 that all refer to entity 3, and it will create two batches
+ // of those. In the first batch, they'll forward-reference. In the second batch, they should backward-reference, except for
+ // the second entity 3, which will now refer to itself as the last one that's been spawned.
+ constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceLast;
+ constexpr size_t NumEntities = 4;
+ FillSpawnable(NumEntities);
+ CreateEntityReferences(refScheme);
+
+ auto callback =
+ [this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ size_t numElements = entities.size();
+
+ for (size_t i = 0; i < numElements; ++i)
+ {
+ const AZ::Entity* const entity = *(entities.begin() + i);
+
+ auto component = entity->FindComponent();
+ ASSERT_NE(nullptr, component);
+ AZ::EntityId comparisonId;
+ if (i < (numElements - 1))
+ {
+ // There are two batches of NumEntities elements. Every entity should either forward-reference or backward-reference
+ // to the last entity of the first batch, except for the very last entity of the second batch, which should reference
+ // itself.
+ comparisonId = (*(entities.begin() + (NumEntities- 1)))->GetId();
+ }
+ else
+ {
+ // The very last entity of the second batch should reference itself because it's now the latest instance of that
+ // entity to be spawned.
+ comparisonId = entity->GetId();
+ }
+
+ EXPECT_EQ(comparisonId, component->m_entityReference);
+ }
+ };
+
+ // Create 2 batches of forward references. In the first batch, entities 0 1 2 will point forward to 3. In the second batch,
+ // entities 0 1 2 will point *backward* to the first 3, and the second entity 3 will point to itself.
+ m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 });
+ m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 });
+ m_manager->ListEntities(*m_ticket, callback);
+ m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
+ }
+
+ TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_MultipleSpawnsInSameCallReferenceCorrectly)
+ {
+ // With SpawnEntities, entity references should either refer to the first entity that *will* be spawned, or the last entity
+ // that *has* been spawned. This test will create entities 0 1 2 3 that all refer to entity 3, and it will create three sets
+ // of those in the same call, with the following results:
+ // - The first 0 1 2 will forward-reference to the first 3
+ // - The first 3 will reference itself
+ // - The second 0 1 2 will backwards-reference to the first 3
+ // - The second 3 will reference itself
+ // - The third 0 1 2 will backwards-reference to the second 3
+ // - The third 3 will reference itself
+ constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceLast;
+ constexpr size_t NumEntities = 4;
+ FillSpawnable(NumEntities);
+ CreateEntityReferences(refScheme);
+
+ auto callback =
+ [this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ size_t numElements = entities.size();
+
+ for (size_t i = 0; i < numElements; ++i)
+ {
+ const AZ::Entity* const entity = *(entities.begin() + i);
+
+ auto component = entity->FindComponent();
+ ASSERT_NE(nullptr, component);
+ AZ::EntityId comparisonId;
+
+ if (i < ((NumEntities * 2) - 1))
+ {
+ // The first 7 entities (0 1 2 3 0 1 2) will all refer to the 4th one (1st '3').
+ comparisonId = (*(entities.begin() + (NumEntities - 1)))->GetId();
+ }
+ else if (i < (numElements - 1))
+ {
+ // The next 4 entities (3 0 1 2) will all refer to the 8th one (2nd '3').
+ comparisonId = (*(entities.begin() + ((NumEntities * 2) - 1)))->GetId();
+ }
+ else
+ {
+ // The very last entity (3) will reference itself (3rd '3').
+ comparisonId = entity->GetId();
+ }
+
+ EXPECT_EQ(comparisonId, component->m_entityReference);
+ }
+ };
+
+ // Create the 3 batches of entities 0, 1, 2, 3. The entity references should work as described at the top of the test.
+ m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3 });
+ m_manager->ListEntities(*m_ticket, callback);
+ m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
+ }
+
+ TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllEntitiesReferenceOtherEntities_OptionalFlagClearsReferenceMap)
+ {
+ constexpr EntityReferenceScheme refScheme = EntityReferenceScheme::AllReferenceLast;
+ constexpr size_t NumEntities = 4;
+ FillSpawnable(NumEntities);
+ CreateEntityReferences(refScheme);
+
+ auto callback =
+ [this, refScheme, NumEntities](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
+ {
+ ValidateEntityReferences(refScheme, NumEntities, entities);
+ };
+
+ // By setting the "referencePreviouslySpawnedEntities" flag to false, the map will get cleared on each call, so in both batches
+ // the entities will forward-reference to the last entity in the batch. If the flag were true, entities 0 1 2 in the second
+ // batch would refer backwards to the first entity 3.
+
+ AzFramework::SpawnEntitiesOptionalArgs optionalArgsSecondBatch;
+ optionalArgsSecondBatch.m_completionCallback = AZStd::move(callback);
+ optionalArgsSecondBatch.m_referencePreviouslySpawnedEntities = false;
+
+ m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 }, optionalArgsSecondBatch);
+ m_manager->SpawnEntities(*m_ticket, { 0, 1, 2, 3 }, AZStd::move(optionalArgsSecondBatch));
+ m_manager->ListEntities(*m_ticket, callback);
+ m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
+ }
+
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash)
{
{
diff --git a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserRequestHandler.cpp b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserRequestHandler.cpp
index e0ba106875..ce29b81a5c 100644
--- a/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserRequestHandler.cpp
+++ b/Code/Sandbox/Editor/AzAssetBrowser/AzAssetBrowserRequestHandler.cpp
@@ -56,7 +56,8 @@ namespace AzAssetBrowserRequestHandlerPrivate
using namespace AzToolsFramework;
using namespace AzToolsFramework::AssetBrowser;
// return true ONLY if we can handle the drop request in the viewport.
- bool CanSpawnEntityForProduct(const ProductAssetBrowserEntry* product)
+ bool CanSpawnEntityForProduct(const ProductAssetBrowserEntry* product,
+ AZStd::optional> optionalProductAssetTypes = AZStd::nullopt)
{
if (!product)
{
@@ -70,7 +71,6 @@ namespace AzAssetBrowserRequestHandlerPrivate
bool canCreateComponent = false;
AZ::AssetTypeInfoBus::EventResult(canCreateComponent, product->GetAssetType(), &AZ::AssetTypeInfo::CanCreateComponent, product->GetAssetId());
-
if (!canCreateComponent)
{
return false;
@@ -78,16 +78,25 @@ namespace AzAssetBrowserRequestHandlerPrivate
AZ::Uuid componentTypeId = AZ::Uuid::CreateNull();
AZ::AssetTypeInfoBus::EventResult(componentTypeId, product->GetAssetType(), &AZ::AssetTypeInfo::GetComponentTypeId);
-
- if (!componentTypeId.IsNull())
+ if (componentTypeId.IsNull())
{
// we have a component type that handles this asset.
- return true;
+ return false;
+ }
+
+ if (optionalProductAssetTypes.has_value())
+ {
+ bool hasConflictingProducts = false;
+ AZ::AssetTypeInfoBus::EventResult(hasConflictingProducts, product->GetAssetType(), &AZ::AssetTypeInfo::HasConflictingProducts, optionalProductAssetTypes.value());
+ if (hasConflictingProducts)
+ {
+ return false;
+ }
}
// additional operations can be added here.
- return false;
+ return true;
}
void SpawnEntityAtPoint(const ProductAssetBrowserEntry* product, AzQtComponents::ViewportDragContext* viewportDragContext, EntityIdList& spawnList, AzFramework::SliceInstantiationTicket& spawnTicket)
@@ -511,9 +520,16 @@ void AzAssetBrowserRequestHandler::Drop(QDropEvent* event, AzQtComponents::DragA
}
// Handle products
+ AZStd::vector productAssetTypes;
+ productAssetTypes.reserve(products.size());
+ for (const AzToolsFramework::AssetBrowser::ProductAssetBrowserEntry* entry : products)
+ {
+ productAssetTypes.emplace_back(entry->GetAssetType());
+ }
+
for (const ProductAssetBrowserEntry* product : products)
{
- if (CanSpawnEntityForProduct(product))
+ if (CanSpawnEntityForProduct(product, productAssetTypes))
{
SpawnEntityAtPoint(product, viewportDragContext, spawnedEntities, spawnTicket);
}
diff --git a/Code/Sandbox/Editor/CMakeLists.txt b/Code/Sandbox/Editor/CMakeLists.txt
index 51620c6e37..6a5fb7c6c8 100644
--- a/Code/Sandbox/Editor/CMakeLists.txt
+++ b/Code/Sandbox/Editor/CMakeLists.txt
@@ -128,6 +128,7 @@ ly_add_target(
Legacy::EditorCore
RUNTIME_DEPENDENCIES
Gem::AtomViewportDisplayInfo
+ Legacy::EditorCommon
)
ly_add_source_properties(
SOURCES CryEdit.cpp
diff --git a/Code/Sandbox/Editor/Style/Editor.qss b/Code/Sandbox/Editor/Style/Editor.qss
index 2e96c73f35..726902bbd9 100644
--- a/Code/Sandbox/Editor/Style/Editor.qss
+++ b/Code/Sandbox/Editor/Style/Editor.qss
@@ -208,22 +208,9 @@ WelcomeScreenDialog QLabel
margin: 0;
}
-WelcomeScreenDialog QLabel#titleLabel
+WelcomeScreenDialog QLabel#currentProjectLabel
{
- font-size: 22px;
- line-height: 32px;
-}
-
-WelcomeScreenDialog QLabel#bodyLabel
-{
- font-size: 14px;
- line-height: 20px;
-}
-
-WelcomeScreenDialog QLabel[fontStyle="sectionTitle"], QLabel#titleLabel[fontStyle="sectionTitle"], QLabel#documentationLink
-{
- font-size: 16px;
- line-height: 24px;
+ margin-top: 10px;
}
WelcomeScreenDialog QPushButton
@@ -232,36 +219,20 @@ WelcomeScreenDialog QPushButton
line-height: 16px;
}
-WelcomeScreenDialog QFrame#viewContainer
-{
- background-color: transparent;
-}
-
-WelcomeScreenDialog QFrame#viewContainer[articleStyle="pinned"]
-{
- background: rgba(180,139,255,5%);
- border: 1px solid #B48BFF;
- box-shadow: 0 0 4px 0 rgba(0,0,0,50%);
-}
-
WelcomeScreenDialog QWidget#articleViewContainerRoot
{
- background: #111111;
+ background: #444444;
}
-WelcomeScreenDialog QScrollArea#previewArea
+WelcomeScreenDialog QWidget#levelViewFTUEContainer
{
- background-color: transparent;
+ background: #282828;
}
-WelcomeScreenDialog QWidget#articleViewContents
-{
- background-color: transparent;
-}
-
-WelcomeScreenDialog QFrame#imageFrame
-{
- background-color: transparent;
+QTableWidget#recentLevelTable::item {
+ background-color: rgb(64,64,64);
+ margin-bottom: 4px;
+ margin-top: 4px;
}
/* Particle Editor */
diff --git a/Code/Sandbox/Editor/ViewportTitleDlg.cpp b/Code/Sandbox/Editor/ViewportTitleDlg.cpp
index dc4815eb6a..5b13b1d308 100644
--- a/Code/Sandbox/Editor/ViewportTitleDlg.cpp
+++ b/Code/Sandbox/Editor/ViewportTitleDlg.cpp
@@ -45,6 +45,7 @@
#include
#include
+#include
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
#include "ui_ViewportTitleDlg.h"
@@ -57,13 +58,16 @@ inline namespace Helpers
{
void ToggleHelpers()
{
- GetIEditor()->GetDisplaySettings()->DisplayHelpers(!GetIEditor()->GetDisplaySettings()->IsDisplayHelpers());
+ const bool newValue = !GetIEditor()->GetDisplaySettings()->IsDisplayHelpers();
+ GetIEditor()->GetDisplaySettings()->DisplayHelpers(newValue);
GetIEditor()->Notify(eNotify_OnDisplayRenderUpdate);
- if (GetIEditor()->GetDisplaySettings()->IsDisplayHelpers() == false)
+ if (newValue == false)
{
GetIEditor()->GetObjectManager()->SendEvent(EVENT_HIDE_HELPER);
}
+ AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Broadcast(
+ &AzToolsFramework::ViewportInteraction::ViewportSettingNotifications::OnDrawHelpersChanged, newValue);
}
bool IsHelpersShown()
@@ -126,6 +130,7 @@ CViewportTitleDlg::CViewportTitleDlg(QWidget* pParent)
SetupCameraDropdownMenu();
SetupResolutionDropdownMenu();
SetupViewportInformationMenu();
+ SetupHelpersButton();
SetupOverflowMenu();
Audio::AudioSystemRequestBus::Broadcast(&Audio::AudioSystemRequestBus::Events::PushRequest, gSettings.bMuteAudio ? m_oMuteAudioRequest : m_oUnmuteAudioRequest);
@@ -207,15 +212,16 @@ void CViewportTitleDlg::SetupViewportInformationMenu()
}
+void CViewportTitleDlg::SetupHelpersButton()
+{
+ connect(m_ui->m_helpers, &QToolButton::clicked, this, &CViewportTitleDlg::OnToggleHelpers);
+ m_ui->m_helpers->setChecked(Helpers::IsHelpersShown());
+}
+
void CViewportTitleDlg::SetupOverflowMenu()
{
// Setup the overflow menu
QMenu* overFlowMenu = new QMenu(this);
- m_debugHelpersAction = new QAction("Debug Helpers", overFlowMenu);
- m_debugHelpersAction->setCheckable(true);
- m_debugHelpersAction->setChecked(Helpers::IsHelpersShown());
- connect(m_debugHelpersAction, &QAction::triggered, this, &CViewportTitleDlg::OnToggleHelpers);
- overFlowMenu->addAction(m_debugHelpersAction);
m_audioMuteAction = new QAction("Mute Audio", overFlowMenu);
connect(m_audioMuteAction, &QAction::triggered, this, &CViewportTitleDlg::OnBnClickedMuteAudio);
@@ -329,7 +335,7 @@ void CViewportTitleDlg::OnMaximize()
void CViewportTitleDlg::OnToggleHelpers()
{
Helpers::ToggleHelpers();
- m_debugHelpersAction->setChecked(Helpers::IsHelpersShown());
+ m_ui->m_helpers->setChecked(Helpers::IsHelpersShown());
}
void CViewportTitleDlg::SetNoViewportInfo()
@@ -755,7 +761,7 @@ void CViewportTitleDlg::OnEditorNotifyEvent(EEditorNotifyEvent event)
switch (event)
{
case eNotify_OnDisplayRenderUpdate:
- m_debugHelpersAction->setChecked(Helpers::IsHelpersShown());
+ m_ui->m_helpers->setChecked(Helpers::IsHelpersShown());
break;
case eNotify_OnBeginGameMode:
case eNotify_OnEndGameMode:
diff --git a/Code/Sandbox/Editor/ViewportTitleDlg.h b/Code/Sandbox/Editor/ViewportTitleDlg.h
index 255354dcbb..e4670e7873 100644
--- a/Code/Sandbox/Editor/ViewportTitleDlg.h
+++ b/Code/Sandbox/Editor/ViewportTitleDlg.h
@@ -102,6 +102,7 @@ protected:
void SetupResolutionDropdownMenu();
void SetupViewportInformationMenu();
void SetupOverflowMenu();
+ void SetupHelpersButton();
QString m_title;
@@ -172,7 +173,6 @@ protected:
QAction* m_normalInformationAction = nullptr;
QAction* m_fullInformationAction = nullptr;
QAction* m_compactInformationAction = nullptr;
- QAction* m_debugHelpersAction = nullptr;
QAction* m_audioMuteAction = nullptr;
QAction* m_enableVRAction = nullptr;
QAction* m_enableGridSnappingAction = nullptr;
diff --git a/Code/Sandbox/Editor/ViewportTitleDlg.ui b/Code/Sandbox/Editor/ViewportTitleDlg.ui
index 2d547bfa99..7d8e9d50d4 100644
--- a/Code/Sandbox/Editor/ViewportTitleDlg.ui
+++ b/Code/Sandbox/Editor/ViewportTitleDlg.ui
@@ -81,6 +81,18 @@
+ -
+
+
+
+ :/Menu/helpers.svg:/Menu/helpers.svg
+
+
+
+ true
+
+
+
-
@@ -94,7 +106,7 @@
- :/stylesheet/img/UI20/menu-centered.svg:/stylesheet/img/UI20/menu-centered.svg
+ :/Menu/menu.svg:/Menu/menu.svg
diff --git a/Code/Sandbox/Editor/WelcomeScreen/DefaultActiveProject.png b/Code/Sandbox/Editor/WelcomeScreen/DefaultActiveProject.png
new file mode 100644
index 0000000000..89c3a7cd47
--- /dev/null
+++ b/Code/Sandbox/Editor/WelcomeScreen/DefaultActiveProject.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:263e95489560dac6e5944ef3caba13e598f83ddead324b943ad7735ba015e1a9
+size 70727
diff --git a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.cpp b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.cpp
index 6faf29dc8e..fa0b2d8135 100644
--- a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.cpp
+++ b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.cpp
@@ -15,7 +15,8 @@
#include "WelcomeScreenDialog.h"
// Qt
-#include
+#include
+#include
#include
#include
#include
@@ -24,6 +25,7 @@
#include
#include
#include
+#include
#include
@@ -74,65 +76,39 @@ static int GetSmallestScreenHeight()
WelcomeScreenDialog::WelcomeScreenDialog(QWidget* pParent)
: QDialog(new WindowDecorationWrapper(WindowDecorationWrapper::OptionAutoAttach | WindowDecorationWrapper::OptionAutoTitleBarButtons, pParent), Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowCloseButtonHint | Qt::WindowTitleHint)
, ui(new Ui::WelcomeScreenDialog)
- , m_pRecentListModel(new QStringListModel(this))
, m_pRecentList(nullptr)
{
ui->setupUi(this);
- // Make our welcome screen checkboxes appear as toggle switches
- AzQtComponents::CheckBox::applyToggleSwitchStyle(ui->autoLoadLevel);
- AzQtComponents::CheckBox::applyToggleSwitchStyle(ui->showOnStartup);
+ ui->recentLevelTable->setColumnCount(3);
+ ui->recentLevelTable->setMouseTracking(true);
+ ui->recentLevelTable->setContextMenuPolicy(Qt::CustomContextMenu);
+ ui->recentLevelTable->horizontalHeader()->hide();
+ ui->recentLevelTable->verticalHeader()->hide();
+ ui->recentLevelTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ ui->recentLevelTable->setSelectionMode(QAbstractItemView::SingleSelection);
+ ui->recentLevelTable->setIconSize(QSize(20, 20));
+ installEventFilter(this);
- ui->autoLoadLevel->setChecked(gSettings.bAutoloadLastLevelAtStartup);
- ui->showOnStartup->setChecked(!gSettings.bShowDashboardAtStartup);
-
- ui->recentLevelList->setModel(m_pRecentListModel);
- ui->recentLevelList->setMouseTracking(true);
- ui->recentLevelList->setContextMenuPolicy(Qt::CustomContextMenu);
-
- auto currentProjectButtonMenu = new QMenu();
-
- ui->currentProjectButton->setMenu(currentProjectButtonMenu);
auto projectName = AZ::Utils::GetProjectName();
- ui->currentProjectButton->setText(projectName.c_str());
- ui->currentProjectButton->adjustSize();
- ui->currentProjectButton->setMinimumWidth(ui->currentProjectButton->width() + 40);
+ ui->currentProjectName->setText(projectName.c_str());
- ui->documentationLink->setCursor(Qt::PointingHandCursor);
- ui->documentationLink->installEventFilter(this);
+ ui->newLevelButton->setDefault(true);
- connect(ui->recentLevelList, &QWidget::customContextMenuRequested, this, &WelcomeScreenDialog::OnShowContextMenu);
+ // Hide these buttons until the new functionality is added
+ ui->gridButton->hide();
+ ui->objectListButton->hide();
+ ui->switchProjectButton->hide();
- connect(ui->recentLevelList, &QListView::entered, this, &WelcomeScreenDialog::OnShowToolTip);
- connect(ui->recentLevelList, &QListView::clicked, this, &WelcomeScreenDialog::OnRecentLevelListItemClicked);
+ connect(ui->recentLevelTable, &QWidget::customContextMenuRequested, this, &WelcomeScreenDialog::OnShowContextMenu);
+
+ connect(ui->recentLevelTable, &QTableWidget::entered, this, &WelcomeScreenDialog::OnShowToolTip);
+ connect(ui->recentLevelTable, &QTableWidget::clicked, this, &WelcomeScreenDialog::OnRecentLevelTableItemClicked);
connect(ui->newLevelButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnNewLevelBtnClicked);
+ connect(ui->levelFileLabel, &QLabel::linkActivated, this, &WelcomeScreenDialog::OnNewLevelLabelClicked);
connect(ui->openLevelButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnOpenLevelBtnClicked);
- connect(ui->newSliceButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnNewSliceBtnClicked);
- connect(ui->openSliceButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnOpenSliceBtnClicked);
-
- connect(ui->documentationButton, &QPushButton::clicked, this, &WelcomeScreenDialog::OnDocumentationBtnClicked);
- connect(ui->showOnStartup, &QCheckBox::clicked, this, &WelcomeScreenDialog::OnShowOnStartupBtnClicked);
- connect(ui->autoLoadLevel, &QCheckBox::clicked, this, &WelcomeScreenDialog::OnAutoLoadLevelBtnClicked);
-
- m_manifest = new News::ResourceManifest(
- std::bind(&WelcomeScreenDialog::SyncSuccess, this),
- std::bind(&WelcomeScreenDialog::SyncFail, this, std::placeholders::_1),
- std::bind(&WelcomeScreenDialog::SyncUpdate, this, std::placeholders::_1, std::placeholders::_2));
-
- m_articleViewContainer = new News::ArticleViewContainer(this, *m_manifest);
- connect(m_articleViewContainer, &News::ArticleViewContainer::scrolled,
- this, &WelcomeScreenDialog::previewAreaScrolled);
- ui->articleViewContainerRoot->layout()->addWidget(m_articleViewContainer);
-
- m_manifest->Sync();
-
-#ifndef ENABLE_SLICE_EDITOR
- ui->newSliceButton->hide();
- ui->openSliceButton->hide();
-#endif
-
// Adjust the height, if need be
// Do it in the constructor so that the WindowDecoratorWrapper handles it correctly
int smallestHeight = GetSmallestScreenHeight();
@@ -153,16 +129,10 @@ WelcomeScreenDialog::WelcomeScreenDialog(QWidget* pParent)
WelcomeScreenDialog::~WelcomeScreenDialog()
{
delete ui;
- delete m_manifest;
}
void WelcomeScreenDialog::done(int result)
{
- if (m_waitingOnAsync)
- {
- m_manifest->Abort();
- }
-
QDialog::done(result);
}
@@ -173,13 +143,11 @@ const QString& WelcomeScreenDialog::GetLevelPath()
bool WelcomeScreenDialog::eventFilter(QObject *watched, QEvent *event)
{
- if (watched == ui->documentationLink)
+ if (event->type() == QEvent::Show)
{
- if (event->type() == QEvent::MouseButtonRelease)
- {
- OnDocumentationBtnClicked(false);
- return true;
- }
+ ui->recentLevelTable->horizontalHeader()->resizeSection(0, ui->nameLabel->width());
+ ui->recentLevelTable->horizontalHeader()->resizeSection(1, ui->modifiedLabel->width());
+ ui->recentLevelTable->horizontalHeader()->resizeSection(2, ui->typeLabel->width());
}
return QDialog::eventFilter(watched, event);
@@ -207,7 +175,9 @@ void WelcomeScreenDialog::SetRecentFileList(RecentFileList* pList)
int nCurDir = sCurDir.length();
int recentListSize = pList->GetSize();
- for (int i = 0; i < recentListSize; ++i)
+ int currentRow = 0;
+ ui->recentLevelTable->setRowCount(recentListSize);
+ for (int i = 0; i < recentListSize; ++i)
{
const QString& recentFile = pList->m_arrNames[i];
if (recentFile.endsWith(m_levelExtension))
@@ -218,7 +188,7 @@ void WelcomeScreenDialog::SetRecentFileList(RecentFileList* pList)
if (sCurEntryDir.compare(sCurDir, Qt::CaseInsensitive) == 0)
{
QString fullPath = recentFile;
- QString name = Path::GetFileName(fullPath);
+ const QString name = Path::GetFile(fullPath);
Path::ConvertSlashToBackSlash(fullPath);
fullPath = Path::ToUnixPath(fullPath.toLower());
@@ -226,18 +196,34 @@ void WelcomeScreenDialog::SetRecentFileList(RecentFileList* pList)
if (fullPath.contains(gamePath))
{
- m_pRecentListModel->setStringList(m_pRecentListModel->stringList() << QString(name));
+ if (gSettings.prefabSystem)
+ {
+ QIcon icon;
+ icon.addFile(QString::fromUtf8(":/Level/level.svg"), QSize(), QIcon::Normal, QIcon::Off);
+ ui->recentLevelTable->setItem(currentRow, 0, new QTableWidgetItem(icon, name));
+ }
+ else
+ {
+ ui->recentLevelTable->setItem(currentRow, 0, new QTableWidgetItem(name));
+ }
+ QFileInfo file(recentFile);
+ QDateTime dateTime = file.lastModified();
+ QString date = QLocale::system().toString(dateTime.date(), QLocale::ShortFormat) + " " +
+ QLocale::system().toString(dateTime.time(), QLocale::LongFormat);
+ ui->recentLevelTable->setItem(currentRow, 1, new QTableWidgetItem(date));
+ ui->recentLevelTable->setItem(currentRow++, 2, new QTableWidgetItem(tr("Level")));
m_levels.push_back(std::make_pair(name, recentFile));
}
}
}
}
}
+ ui->recentLevelTable->setRowCount(currentRow);
+ ui->recentLevelTable->setMinimumHeight(currentRow * ui->recentLevelTable->verticalHeader()->defaultSectionSize());
+ ui->recentLevelTable->setMaximumHeight(currentRow * ui->recentLevelTable->verticalHeader()->defaultSectionSize());
+ ui->levelFileLabel->setVisible(currentRow ? false : true);
- ui->recentLevelList->setCurrentIndex(QModelIndex());
- int rowSize = ui->recentLevelList->sizeHintForRow(0) + ui->recentLevelList->spacing() * 2;
- ui->recentLevelList->setMinimumHeight(m_pRecentListModel->rowCount() * rowSize);
- ui->recentLevelList->setMaximumHeight(m_pRecentListModel->rowCount() * rowSize);
+ ui->recentLevelTable->setCurrentIndex(QModelIndex());
}
@@ -245,7 +231,7 @@ void WelcomeScreenDialog::RemoveLevelEntry(int index)
{
TNamePathPair levelPath = m_levels[index];
- m_pRecentListModel->removeRow(index);
+ ui->recentLevelTable->removeRow(index);
m_levels.erase(m_levels.begin() + index);
@@ -284,21 +270,18 @@ void WelcomeScreenDialog::OnShowToolTip(const QModelIndex& index)
{
const QString& fullPath = m_levels[index.row()].second;
- //TEMPORARY:Begin This can be put back once the main window is in Qt
- //QRect itemRect = ui->recentLevelList->visualRect(index);
- QToolTip::showText(QCursor::pos(), QString("Open level: %1").arg(fullPath) /*, ui->recentLevelList, itemRect*/);
- //TEMPORARY:END
+ QToolTip::showText(QCursor::pos(), QString("Open level: %1").arg(fullPath));
}
void WelcomeScreenDialog::OnShowContextMenu(const QPoint& pos)
{
- QModelIndex index = ui->recentLevelList->indexAt(pos);
+ QModelIndex index = ui->recentLevelTable->indexAt(pos);
if (index.isValid())
{
- QString level = m_pRecentListModel->data(index, 0).toString();
+ QString level = ui->recentLevelTable->itemAt(pos)->text();
- QPoint globalPos = ui->recentLevelList->viewport()->mapToGlobal(pos);
+ QPoint globalPos = ui->recentLevelTable->viewport()->mapToGlobal(pos);
QMenu contextMenu;
contextMenu.addAction(QString("Remove " + level + " from recent list"));
@@ -310,13 +293,16 @@ void WelcomeScreenDialog::OnShowContextMenu(const QPoint& pos)
}
}
-
void WelcomeScreenDialog::OnNewLevelBtnClicked([[maybe_unused]] bool checked)
{
m_levelPath = "new";
accept();
}
+void WelcomeScreenDialog::OnNewLevelLabelClicked([[maybe_unused]] const QString& path)
+{
+ OnNewLevelBtnClicked(true);
+}
void WelcomeScreenDialog::OnOpenLevelBtnClicked([[maybe_unused]] bool checked)
{
@@ -329,27 +315,7 @@ void WelcomeScreenDialog::OnOpenLevelBtnClicked([[maybe_unused]] bool checked)
}
}
-void WelcomeScreenDialog::OnNewSliceBtnClicked([[maybe_unused]] bool checked)
-{
- m_levelPath = "new slice";
- accept();
-}
-
-void WelcomeScreenDialog::OnOpenSliceBtnClicked(bool)
-{
- QString fileName = QFileDialog::getOpenFileName(MainWindow::instance(),
- tr("Open Slice"),
- Path::GetEditingGameDataFolder().c_str(),
- tr("Slice (*.slice)"));
-
- if (!fileName.isEmpty())
- {
- m_levelPath = fileName;
- accept();
- }
-}
-
-void WelcomeScreenDialog::OnRecentLevelListItemClicked(const QModelIndex& modelIndex)
+void WelcomeScreenDialog::OnRecentLevelTableItemClicked(const QModelIndex& modelIndex)
{
int index = modelIndex.row();
@@ -365,45 +331,6 @@ void WelcomeScreenDialog::OnCloseBtnClicked([[maybe_unused]] bool checked)
accept();
}
-void WelcomeScreenDialog::OnAutoLoadLevelBtnClicked(bool checked)
-{
- gSettings.bAutoloadLastLevelAtStartup = checked;
- gSettings.Save();
-}
-
-
-void WelcomeScreenDialog::OnShowOnStartupBtnClicked(bool checked)
-{
- gSettings.bShowDashboardAtStartup = !checked;
- gSettings.Save();
-
- if (gSettings.bShowDashboardAtStartup == false)
- {
- QMessageBox msgBox(AzToolsFramework::GetActiveWindow());
- msgBox.setWindowTitle(QObject::tr("Skip the Welcome dialog on startup"));
- msgBox.setText(QObject::tr("You may re-enable the Welcome dialog at any time by going to Edit > Editor Settings > Global Preferences in the menu bar."));
- msgBox.exec();
- }
-}
-
-void WelcomeScreenDialog::OnDocumentationBtnClicked([[maybe_unused]] bool checked)
-{
- QString webLink = tr("https://aws.amazon.com/lumberyard/support/");
- QDesktopServices::openUrl(QUrl(webLink));
-}
-
-void WelcomeScreenDialog::SyncFail([[maybe_unused]] News::ErrorCode error)
-{
- m_articleViewContainer->AddErrorMessage();
- m_waitingOnAsync = false;
-}
-
-void WelcomeScreenDialog::SyncSuccess()
-{
- m_articleViewContainer->PopulateArticles();
- m_waitingOnAsync = false;
-}
-
void WelcomeScreenDialog::previewAreaScrolled()
{
//this should only be reported once per session
diff --git a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.h b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.h
index 73e9d75ea3..a3460630ac 100644
--- a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.h
+++ b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.h
@@ -52,13 +52,9 @@ private:
Ui::WelcomeScreenDialog* ui;
QString m_levelPath;
- QStringListModel* m_pRecentListModel;
TNameFullPathArray m_levels;
RecentFileList* m_pRecentList;
- News::ResourceManifest* m_manifest = nullptr;
- News::ArticleViewContainer* m_articleViewContainer = nullptr;
const char* m_levelExtension = nullptr;
- bool m_waitingOnAsync = true;
bool m_messageScrollReported = false;
void RemoveLevelEntry(int index);
@@ -66,19 +62,11 @@ private:
void OnShowToolTip(const QModelIndex& index);
void OnShowContextMenu(const QPoint& point);
void OnNewLevelBtnClicked(bool checked);
+ void OnNewLevelLabelClicked(const QString& checked);
void OnOpenLevelBtnClicked(bool checked);
- void OnNewSliceBtnClicked(bool checked);
- void OnOpenSliceBtnClicked(bool checked);
- void OnRecentLevelListItemClicked(const QModelIndex& index);
- void OnGettingStartedBtnClicked(bool checked);
- void OnTutorialsBtnClicked(bool checked);
- void OnDocumentationBtnClicked(bool checked);
- void OnForumsBtnClicked(bool checked);
- void OnAutoLoadLevelBtnClicked(bool checked);
- void OnShowOnStartupBtnClicked(bool checked);
+ void OnRecentLevelTableItemClicked(const QModelIndex& index);
void OnCloseBtnClicked(bool checked);
- void SyncUpdate(const QString& /* message */, News::LogType /* logType */) {}
void SyncFail(News::ErrorCode error);
void SyncSuccess();
diff --git a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.qrc b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.qrc
index b6fa8150c5..9e8ff62f48 100644
--- a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.qrc
+++ b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.qrc
@@ -1,5 +1,5 @@
- WelcomeScreenDialogHeader.png
+ DefaultActiveProject.png
diff --git a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.ui b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.ui
index be0d175a09..680b411121 100644
--- a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.ui
+++ b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialog.ui
@@ -2,12 +2,15 @@
WelcomeScreenDialog
+
+ true
+
0
0
- 800
- 600
+ 945
+ 639
@@ -18,21 +21,21 @@
- 800
- 600
+ 945
+ 639
- 800
- 16777215
+ 945
+ 639
Qt::TabFocus
- Welcome to Open 3D Engine
+ Welcome to O3DE
@@ -53,100 +56,6 @@
0
-
-
-
-
-
- 0
- 36
-
-
-
-
- 16777215
- 36
-
-
-
-
- 10
-
-
- 16
-
-
- 0
-
-
- 12
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
- Current project:
-
-
-
- -
-
-
- Current Project Name
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
- 16777215
- 1
-
-
-
- color: "black"
-
-
- QFrame::Plain
-
-
- 0
-
-
- Qt::Horizontal
-
-
-
-
@@ -165,20 +74,26 @@
0
-
-
+
+
+
+ 0
+ 0
+
+
- 0
+ 183
0
- 320
+ 183
16777215
-
+
0
@@ -191,6 +106,143 @@
0
+
+ 10
+
+
-
+
+
+ 15
+
+
-
+
+
+ 10
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Active project
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 126
+ 167
+
+
+
+
+ 126
+ 167
+
+
+
+
+
+
+ :/WelcomeScreenDialog/DefaultActiveProject.png
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ MyGame
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ 15
+
+
+ 15
+
+
-
+
+
+ Switch project...
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 762
+ 0
+
+
+
+
+ 762
+ 16777215
+
+
+
+
+ 0
+
+
+ 20
+
+
+ 0
+
+
+ 20
+
0
@@ -227,7 +279,7 @@
- Open or create a level
+ Recent Files
-1
@@ -255,16 +307,6 @@
0
- -
-
-
- Qt::ScrollBarAlwaysOff
-
-
- 4
-
-
-
@@ -310,8 +352,26 @@
-
+
+
+ 0
+ 0
+
+
+
+
+ 156
+ 0
+
+
+
+
+ 156
+ 16777215
+
+
- New level...
+ Create new...
@@ -333,8 +393,67 @@
-
+
+
+ 156
+ 0
+
+
+
+
+ 156
+ 16777215
+
+
- Open level...
+ Open...
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ ...
+
+
+
+ :/stylesheet/img/UI20/toolbar/Object_list.svg:/stylesheet/img/UI20/toolbar/Object_list.svg
+
+
+
+ 24
+ 24
+
+
+
+
+ -
+
+
+ ...
+
+
+
+ :/stylesheet/img/UI20/toolbar/Grid.svg:/stylesheet/img/UI20/toolbar/Grid.svg
+
+
+
+ 24
+ 24
+
@@ -358,65 +477,130 @@
-
-
-
-
- 0
- 0
-
+
+
+ 6
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
-
- New slice...
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QSizePolicy::Fixed
-
-
-
- 24
- 0
-
-
-
-
- -
-
-
- Open slice...
-
-
-
-
-
+ -
+
+
+ Name
+
+
+
+ -
+
+
+ Last modified
+
+
+
+ -
+
+
+ Type
+
+
+
+
+ -
+
+
+ 16
+
+
+ 16
+
+
+ 16
+
+
-
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+ No level file created yet for this project. <a href="#">Create one</a> now.
+
+
+ Qt::RichText
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ 3
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ 1
+
+
+ 48
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
-
@@ -439,205 +623,16 @@
- -
-
-
-
- 0
- 48
-
-
-
-
- 16777215
- 48
-
-
-
-
- 10
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
-
-
- 24
- 24
-
-
-
- info
-
-
-
- :/stylesheet/img/UI20/Info.svg:/stylesheet/img/UI20/Info.svg
-
-
-
- -
-
-
- Documentation and tutorials
-
-
- link
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 1
- 16777215
-
-
-
- color: "black"
-
-
- QFrame::Plain
-
-
- 0
-
-
- Qt::Vertical
-
-
-
- -
-
-
-
- 480
- 0
-
-
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
- -
-
-
-
- 16777215
- 1
-
-
-
- color: "black"
-
-
- QFrame::Plain
-
-
- 0
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
- 0
- 36
-
-
-
-
- 16777215
- 36
-
-
-
-
- 30
-
-
- 16
-
-
- 0
-
-
- 16
-
-
- 0
-
-
-
-
-
-
- 0
- 0
-
-
-
- Auto-load last opened level on startup
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Skip this dialog on startup
-
-
-
-
-
-
+
diff --git a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialogHeader.png b/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialogHeader.png
deleted file mode 100644
index e2656e3dfd..0000000000
--- a/Code/Sandbox/Editor/WelcomeScreen/WelcomeScreenDialogHeader.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:53b846352880d940621b14b1ea9514e0a4c95aa6ead4d00234a98684c061c04f
-size 29505
diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc
index 33acfa9e1b..014a098a91 100644
--- a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc
+++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc
@@ -31,5 +31,6 @@
CarrotArrowDown.svg
Summary.svg
WindowClose.svg
+ Warning.svg
diff --git a/Code/Tools/ProjectManager/Resources/Warning.svg b/Code/Tools/ProjectManager/Resources/Warning.svg
new file mode 100644
index 0000000000..28f7bc5f42
--- /dev/null
+++ b/Code/Tools/ProjectManager/Resources/Warning.svg
@@ -0,0 +1,5 @@
+
diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp
index 0e3a4ba95d..93f8895079 100644
--- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp
+++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp
@@ -206,7 +206,11 @@ namespace O3DE::ProjectManager
PythonBindingsInterface::Get()->AddProject(projectInfo.m_path);
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
- m_gemCatalogScreen->EnableDisableGemsForProject(projectInfo.m_path);
+ if (!m_gemCatalogScreen->EnableDisableGemsForProject(projectInfo.m_path))
+ {
+ QMessageBox::critical(this, tr("Failed to configure gems"), tr("Failed to configure gems for template."));
+ return;
+ }
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
projectInfo.m_needsBuild = true;
diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp
index cf597745ea..4efa9d1d0f 100644
--- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp
+++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp
@@ -16,7 +16,7 @@
#include
#include
#include
-#include
+#include
#include
#include
@@ -46,28 +46,28 @@ namespace O3DE::ProjectManager
m_engineVersion->lineEdit()->setReadOnly(true);
layout->addWidget(m_engineVersion);
- m_thirdParty = new FormBrowseEditWidget(tr("3rd Party Software Folder"), engineInfo.m_thirdPartyPath, this);
+ m_thirdParty = new FormFolderBrowseEditWidget(tr("3rd Party Software Folder"), engineInfo.m_thirdPartyPath, this);
m_thirdParty->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_thirdParty->lineEdit()->setReadOnly(true);
m_thirdParty->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_thirdParty->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_thirdParty);
- m_defaultProjects = new FormBrowseEditWidget(tr("Default Projects Folder"), engineInfo.m_defaultProjectsFolder, this);
+ m_defaultProjects = new FormFolderBrowseEditWidget(tr("Default Projects Folder"), engineInfo.m_defaultProjectsFolder, this);
m_defaultProjects->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultProjects->lineEdit()->setReadOnly(true);
m_defaultProjects->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_defaultProjects->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_defaultProjects);
- m_defaultGems = new FormBrowseEditWidget(tr("Default Gems Folder"), engineInfo.m_defaultGemsFolder, this);
+ m_defaultGems = new FormFolderBrowseEditWidget(tr("Default Gems Folder"), engineInfo.m_defaultGemsFolder, this);
m_defaultGems->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultGems->lineEdit()->setReadOnly(true);
m_defaultGems->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_defaultGems->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_defaultGems);
- m_defaultProjectTemplates = new FormBrowseEditWidget(tr("Default Project Templates Folder"), engineInfo.m_defaultTemplatesFolder, this);
+ m_defaultProjectTemplates = new FormFolderBrowseEditWidget(tr("Default Project Templates Folder"), engineInfo.m_defaultTemplatesFolder, this);
m_defaultProjectTemplates->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultProjectTemplates->lineEdit()->setReadOnly(true);
m_defaultProjectTemplates->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp
index c30d6a7b30..9a2227cf4a 100644
--- a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp
+++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp
@@ -11,13 +11,9 @@
*/
#include
-#include
+
#include
#include
-#include
-#include
-#include
-#include
namespace O3DE::ProjectManager
{
@@ -30,20 +26,4 @@ namespace O3DE::ProjectManager
connect(browseButton, &QPushButton::pressed, this, &FormBrowseEditWidget::HandleBrowseButton);
m_frameLayout->addWidget(browseButton);
}
-
- void FormBrowseEditWidget::HandleBrowseButton()
- {
- QString defaultPath = m_lineEdit->text();
- if (defaultPath.isEmpty())
- {
- defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
- }
-
- QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath));
- if (!directory.isEmpty())
- {
- m_lineEdit->setText(directory);
- }
-
- }
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h
index 887fc29dd9..1eba97654b 100644
--- a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h
+++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h
@@ -27,7 +27,7 @@ namespace O3DE::ProjectManager
explicit FormBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
~FormBrowseEditWidget() = default;
- private slots:
- void HandleBrowseButton();
+ protected slots:
+ virtual void HandleBrowseButton() = 0;
};
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/FormFolderBrowseEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormFolderBrowseEditWidget.cpp
new file mode 100644
index 0000000000..2c4cad5adb
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/FormFolderBrowseEditWidget.cpp
@@ -0,0 +1,42 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include
+#include
+
+#include
+#include
+#include
+
+namespace O3DE::ProjectManager
+{
+ FormFolderBrowseEditWidget::FormFolderBrowseEditWidget(const QString& labelText, const QString& valueText, QWidget* parent)
+ : FormBrowseEditWidget(labelText, valueText, parent)
+ {
+ }
+
+ void FormFolderBrowseEditWidget::HandleBrowseButton()
+ {
+ QString defaultPath = m_lineEdit->text();
+ if (defaultPath.isEmpty())
+ {
+ defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
+ }
+
+ QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath));
+ if (!directory.isEmpty())
+ {
+ m_lineEdit->setText(directory);
+ }
+
+ }
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/FormFolderBrowseEditWidget.h b/Code/Tools/ProjectManager/Source/FormFolderBrowseEditWidget.h
new file mode 100644
index 0000000000..a99fe3b5c2
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/FormFolderBrowseEditWidget.h
@@ -0,0 +1,33 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include
+#endif
+
+namespace O3DE::ProjectManager
+{
+ class FormFolderBrowseEditWidget
+ : public FormBrowseEditWidget
+ {
+ Q_OBJECT
+
+ public:
+ explicit FormFolderBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
+ ~FormFolderBrowseEditWidget() = default;
+
+ protected:
+ void HandleBrowseButton() override;
+ };
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/FormImageBrowseEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormImageBrowseEditWidget.cpp
new file mode 100644
index 0000000000..5bb9d61dd6
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/FormImageBrowseEditWidget.cpp
@@ -0,0 +1,35 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+
+#include
+#include
+
+#include
+#include
+
+namespace O3DE::ProjectManager
+{
+ FormImageBrowseEditWidget::FormImageBrowseEditWidget(const QString& labelText, const QString& valueText, QWidget* parent)
+ : FormBrowseEditWidget(labelText, valueText, parent)
+ {
+ }
+
+ void FormImageBrowseEditWidget::HandleBrowseButton()
+ {
+ QString file = QDir::toNativeSeparators(QFileDialog::getOpenFileName(
+ this, tr("Select Image"), m_lineEdit->text(), tr("PNG (*.png)")));
+ if (!file.isEmpty())
+ {
+ m_lineEdit->setText(file);
+ }
+ }
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/FormImageBrowseEditWidget.h b/Code/Tools/ProjectManager/Source/FormImageBrowseEditWidget.h
new file mode 100644
index 0000000000..e01e9254f8
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/FormImageBrowseEditWidget.h
@@ -0,0 +1,33 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include
+#endif
+
+namespace O3DE::ProjectManager
+{
+ class FormImageBrowseEditWidget
+ : public FormBrowseEditWidget
+ {
+ Q_OBJECT
+
+ public:
+ explicit FormImageBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
+ ~FormImageBrowseEditWidget() = default;
+
+ protected:
+ void HandleBrowseButton() override;
+ };
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp
index 2c92af4e51..763c9dfb75 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -144,12 +145,23 @@ namespace O3DE::ProjectManager
}
}
- void GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
+ bool GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
{
IPythonBindings* pythonBindings = PythonBindingsInterface::Get();
QVector toBeAdded = m_gemModel->GatherGemsToBeAdded();
QVector toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
+ if (m_gemModel->DoGemsToBeAddedHaveRequirements())
+ {
+ GemRequirementDialog* confirmRequirementsDialog = new GemRequirementDialog(m_gemModel, toBeAdded, this);
+ confirmRequirementsDialog->exec();
+
+ if (confirmRequirementsDialog->GetButtonResult() != QDialogButtonBox::ApplyRole)
+ {
+ return false;
+ }
+ }
+
for (const QModelIndex& modelIndex : toBeAdded)
{
const QString gemPath = GemModel::GetPath(modelIndex);
@@ -158,6 +170,8 @@ namespace O3DE::ProjectManager
{
QMessageBox::critical(nullptr, "Operation failed",
QString("Cannot add gem %1 to project.\n\nError:\n%2").arg(GemModel::GetName(modelIndex), result.GetError().c_str()));
+
+ return false;
}
}
@@ -169,8 +183,12 @@ namespace O3DE::ProjectManager
{
QMessageBox::critical(nullptr, "Operation failed",
QString("Cannot remove gem %1 from project.\n\nError:\n%2").arg(GemModel::GetName(modelIndex), result.GetError().c_str()));
+
+ return false;
}
}
+
+ return true;
}
ProjectManagerScreen GemCatalogScreen::GetScreenEnum()
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h
index f5092e837a..fc771d9168 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h
@@ -33,7 +33,7 @@ namespace O3DE::ProjectManager
ProjectManagerScreen GetScreenEnum() override;
void ReinitForProject(const QString& projectPath, bool isNewProject);
- void EnableDisableGemsForProject(const QString& projectPath);
+ bool EnableDisableGemsForProject(const QString& projectPath);
private:
void FillModel(const QString& projectPath, bool isNewProject);
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h
index 722783ece1..f99e9689f3 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h
@@ -73,6 +73,7 @@ namespace O3DE::ProjectManager
Platforms m_platforms;
Types m_types; //! Asset and/or Code and/or Tool
QStringList m_features;
+ QString m_requirement;
QString m_directoryLink;
QString m_documentationLink;
QString m_version = "Unknown Version";
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp
index 3ecc18231e..119ec68d3a 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp
@@ -16,6 +16,7 @@
#include
#include
#include
+#include
namespace O3DE::ProjectManager
{
@@ -70,6 +71,22 @@ namespace O3DE::ProjectManager
m_directoryLinkLabel->SetUrl(m_model->GetDirectoryLink(modelIndex));
m_documentationLinkLabel->SetUrl(m_model->GetDocLink(modelIndex));
+ if (m_model->HasRequirement(modelIndex))
+ {
+ m_reqirementsIconLabel->show();
+ m_reqirementsTitleLabel->show();
+ m_reqirementsTextLabel->show();
+
+ m_reqirementsTitleLabel->setText("Requirement");
+ m_reqirementsTextLabel->setText(m_model->GetRequirement(modelIndex));
+ }
+ else
+ {
+ m_reqirementsIconLabel->hide();
+ m_reqirementsTitleLabel->hide();
+ m_reqirementsTextLabel->hide();
+ }
+
// Depending and conflicting gems
m_dependingGems->Update("Depending Gems", "The following Gems will be automatically enabled with this Gem.", m_model->GetDependingGemNames(modelIndex));
m_conflictingGems->Update("Conflicting Gems", "The following Gems will be automatically disabled with this Gem.", m_model->GetConflictingGemNames(modelIndex));
@@ -134,6 +151,28 @@ namespace O3DE::ProjectManager
m_mainLayout->addSpacing(10);
+ // Requirements
+ m_reqirementsTitleLabel = GemInspector::CreateStyledLabel(m_mainLayout, 16, s_headerColor);
+
+ QHBoxLayout* requrementsLayout = new QHBoxLayout();
+ requrementsLayout->setAlignment(Qt::AlignTop);
+ requrementsLayout->setMargin(0);
+ requrementsLayout->setSpacing(0);
+
+ m_reqirementsIconLabel = new QLabel();
+ m_reqirementsIconLabel->setPixmap(QIcon(":/Warning.svg").pixmap(24, 24));
+ requrementsLayout->addWidget(m_reqirementsIconLabel);
+
+ m_reqirementsTextLabel = GemInspector::CreateStyledLabel(requrementsLayout, 10, s_textColor);
+ m_reqirementsTextLabel->setWordWrap(true);
+
+ QSpacerItem* reqirementsSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding);
+ requrementsLayout->addSpacerItem(reqirementsSpacer);
+
+ m_mainLayout->addLayout(requrementsLayout);
+
+ m_mainLayout->addSpacing(20);
+
// Depending and conflicting gems
m_dependingGems = new GemsSubWidget();
m_mainLayout->addWidget(m_dependingGems);
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h
index 69c065c81e..4363ea3bc1 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h
@@ -76,6 +76,11 @@ namespace O3DE::ProjectManager
LinkLabel* m_directoryLinkLabel = nullptr;
LinkLabel* m_documentationLinkLabel = nullptr;
+ // Requirements
+ QLabel* m_reqirementsTitleLabel = nullptr;
+ QLabel* m_reqirementsIconLabel = nullptr;
+ QLabel* m_reqirementsTextLabel = nullptr;
+
// Depending and conflicting gems
GemsSubWidget* m_dependingGems = nullptr;
GemsSubWidget* m_conflictingGems = nullptr;
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp
index 03787de7e8..6529e4cf3d 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp
@@ -11,7 +11,7 @@
*/
#include
-#include "GemModel.h"
+#include
#include
#include
#include
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h
index 48f173ec3f..a155d9ece0 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h
@@ -62,7 +62,7 @@ namespace O3DE::ProjectManager
inline constexpr static int s_buttonCircleRadius = s_buttonBorderRadius - 2;
inline constexpr static qreal s_buttonFontSize = 10.0;
- private:
+ protected:
void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const;
QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const;
QRect CalcButtonRect(const QRect& contentRect) const;
@@ -71,6 +71,7 @@ namespace O3DE::ProjectManager
QAbstractItemModel* m_model = nullptr;
+ private:
// Platform icons
void AddPlatformIcon(GemInfo::Platform platform, const QString& iconPath);
inline constexpr static int s_platformIconSize = 12;
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp
index 2838277696..575c09db05 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp
@@ -10,11 +10,9 @@
*
*/
-#include "GemListView.h"
-#include "GemItemDelegate.h"
+#include
+#include
#include
-#include
-#include
namespace O3DE::ProjectManager
{
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h
index 178de2395f..5f1a018b9f 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemListView.h
@@ -13,7 +13,6 @@
#pragma once
#if !defined(Q_MOC_RUN)
-#include "GemInfo.h"
#include
#include
#include
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp
index 5dc40723c9..7d9d86e3a7 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp
@@ -49,6 +49,7 @@ namespace O3DE::ProjectManager
item->setData(gemInfo.m_binarySizeInKB, RoleBinarySize);
item->setData(gemInfo.m_features, RoleFeatures);
item->setData(gemInfo.m_path, RolePath);
+ item->setData(gemInfo.m_requirement, RoleRequirement);
appendRow(item);
@@ -183,6 +184,11 @@ namespace O3DE::ProjectManager
return modelIndex.data(RolePath).toString();
}
+ QString GemModel::GetRequirement(const QModelIndex& modelIndex)
+ {
+ return modelIndex.data(RoleRequirement).toString();
+ }
+
bool GemModel::IsAdded(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleIsAdded).toBool();
@@ -208,6 +214,24 @@ namespace O3DE::ProjectManager
return (modelIndex.data(RoleWasPreviouslyAdded).toBool() && !modelIndex.data(RoleIsAdded).toBool());
}
+ bool GemModel::HasRequirement(const QModelIndex& modelIndex)
+ {
+ return !modelIndex.data(RoleRequirement).toString().isEmpty();
+ }
+
+ bool GemModel::DoGemsToBeAddedHaveRequirements() const
+ {
+ for (int row = 0; row < rowCount(); ++row)
+ {
+ const QModelIndex modelIndex = index(row, 0);
+ if (NeedsToBeAdded(modelIndex) && HasRequirement(modelIndex))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
QVector GemModel::GatherGemsToBeAdded() const
{
QVector result;
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h
index 2e05472cdf..301053a9bf 100644
--- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h
@@ -53,12 +53,16 @@ namespace O3DE::ProjectManager
static int GetBinarySizeInKB(const QModelIndex& modelIndex);
static QStringList GetFeatures(const QModelIndex& modelIndex);
static QString GetPath(const QModelIndex& modelIndex);
+ static QString GetRequirement(const QModelIndex& modelIndex);
static bool IsAdded(const QModelIndex& modelIndex);
static void SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded);
static void SetWasPreviouslyAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded);
static bool NeedsToBeAdded(const QModelIndex& modelIndex);
static bool NeedsToBeRemoved(const QModelIndex& modelIndex);
+ static bool HasRequirement(const QModelIndex& modelIndex);
+
+ bool DoGemsToBeAddedHaveRequirements() const;
QVector GatherGemsToBeAdded() const;
QVector GatherGemsToBeRemoved() const;
@@ -84,7 +88,8 @@ namespace O3DE::ProjectManager
RoleBinarySize,
RoleFeatures,
RoleTypes,
- RolePath
+ RolePath,
+ RoleRequirement
};
QHash m_nameToIndexMap;
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.cpp
new file mode 100644
index 0000000000..07024a799b
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.cpp
@@ -0,0 +1,93 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include
+#include
+
+#include
+
+namespace O3DE::ProjectManager
+{
+ GemRequirementDelegate::GemRequirementDelegate(QAbstractItemModel* model, QObject* parent)
+ : GemItemDelegate(model, parent)
+ {
+ }
+
+ void GemRequirementDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const
+ {
+ if (!modelIndex.isValid())
+ {
+ return;
+ }
+
+ QStyleOptionViewItem options(option);
+ initStyleOption(&options, modelIndex);
+
+ painter->save();
+ painter->setRenderHint(QPainter::Antialiasing);
+
+ QRect fullRect, itemRect, contentRect;
+ CalcRects(options, fullRect, itemRect, contentRect);
+
+ QFont standardFont(options.font);
+ standardFont.setPixelSize(s_fontSize);
+ QFontMetrics standardFontMetrics(standardFont);
+
+ painter->save();
+ painter->setClipping(true);
+ painter->setClipRect(fullRect);
+ painter->setFont(options.font);
+
+ // Draw background
+ painter->fillRect(fullRect, m_backgroundColor);
+
+ // Draw item background
+ const QColor itemBackgroundColor = m_itemBackgroundColor;
+ painter->fillRect(itemRect, itemBackgroundColor);
+
+ // Gem name
+ QString gemName = GemModel::GetName(modelIndex);
+ QFont gemNameFont(options.font);
+ const int firstColumnMaxTextWidth = s_summaryStartX - 30;
+ gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
+ gemNameFont.setPixelSize(s_gemNameFontSize);
+ gemNameFont.setBold(true);
+ QRect gemNameRect = GetTextRect(gemNameFont, gemName, s_gemNameFontSize);
+ gemNameRect.moveTo(contentRect.left(), contentRect.center().y() - s_gemNameFontSize);
+
+ painter->setFont(gemNameFont);
+ painter->setPen(m_textColor);
+ painter->drawText(gemNameRect, Qt::TextSingleLine, gemName);
+
+ // Gem requirement
+ const QSize requirementSize = QSize(contentRect.width() - s_summaryStartX - s_itemMargins.right(), contentRect.height());
+ const QRect requirementRect = QRect(QPoint(contentRect.left() + s_summaryStartX, contentRect.top()), requirementSize);
+
+ painter->setFont(standardFont);
+ painter->setPen(m_textColor);
+
+ const QString requirement = GemModel::GetRequirement(modelIndex);
+ painter->drawText(requirementRect, Qt::AlignLeft | Qt::TextWordWrap, requirement);
+
+ painter->restore();
+ }
+
+ bool GemRequirementDelegate::editorEvent(
+ [[maybe_unused]] QEvent* event,
+ [[maybe_unused]] QAbstractItemModel* model,
+ [[maybe_unused]] const QStyleOptionViewItem& option,
+ [[maybe_unused]] const QModelIndex& modelIndex)
+ {
+ // Do nothing here
+ return false;
+ }
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.h
new file mode 100644
index 0000000000..b221dcb8fe
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDelegate.h
@@ -0,0 +1,37 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include
+#endif
+
+
+namespace O3DE::ProjectManager
+{
+ class GemRequirementDelegate
+ : public GemItemDelegate
+ {
+ Q_OBJECT // AUTOMOC
+
+ public:
+ explicit GemRequirementDelegate(QAbstractItemModel* model, QObject* parent = nullptr);
+ ~GemRequirementDelegate() = default;
+
+ void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override;
+ bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) override;
+
+ const QColor m_backgroundColor = QColor("#444444"); // Outside of the actual gem item
+ const QColor m_itemBackgroundColor = QColor("#393939"); // Background color of the gem item
+ };
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.cpp
new file mode 100644
index 0000000000..ad0e64b1ca
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.cpp
@@ -0,0 +1,92 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace O3DE::ProjectManager
+{
+ GemRequirementDialog::GemRequirementDialog(GemModel* model, const QVector& gemsToAdd, QWidget* parent)
+ : QDialog(parent)
+ {
+ setWindowTitle(tr("Manual setup is required"));
+ setModal(true);
+
+ QVBoxLayout* vLayout = new QVBoxLayout();
+ vLayout->setMargin(0);
+ vLayout->setContentsMargins(25, 10, 25, 10);
+ vLayout->setSizeConstraint(QLayout::SetFixedSize);
+ setLayout(vLayout);
+
+ QHBoxLayout* instructionLayout = new QHBoxLayout();
+ instructionLayout->setMargin(0);
+
+ QLabel* instructionIconLabel = new QLabel();
+ instructionIconLabel->setPixmap(QIcon(":/Warning.svg").pixmap(32, 32));
+ instructionLayout->addWidget(instructionIconLabel);
+
+ instructionLayout->addSpacing(10);
+
+ QLabel* instructionLabel = new QLabel(tr("The following Gem(s) require manual setup before the project can be built successfully."));
+ instructionLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ instructionLayout->addWidget(instructionLabel);
+
+ QSpacerItem* instructionSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
+ instructionLayout->addSpacerItem(instructionSpacer);
+
+ vLayout->addLayout(instructionLayout);
+
+ vLayout->addSpacing(20);
+
+ GemRequirementFilterProxyModel* proxModel = new GemRequirementFilterProxyModel(model, gemsToAdd, this);
+
+ GemRequirementListView* m_gemListView = new GemRequirementListView(proxModel, proxModel->GetSelectionModel(), this);
+ vLayout->addWidget(m_gemListView);
+
+ QDialogButtonBox* dialogButtons = new QDialogButtonBox();
+ dialogButtons->setObjectName("footer");
+ vLayout->addWidget(dialogButtons);
+
+ QPushButton* cancelButton = dialogButtons->addButton(tr("Cancel"), QDialogButtonBox::RejectRole);
+ cancelButton->setProperty("secondary", true);
+ QPushButton* continueButton = dialogButtons->addButton(tr("Continue"), QDialogButtonBox::ApplyRole);
+
+ connect(cancelButton, &QPushButton::clicked, this, &GemRequirementDialog::CancelButtonPressed);
+ connect(continueButton, &QPushButton::clicked, this, &GemRequirementDialog::ContinueButtonPressed);
+ }
+
+ QDialogButtonBox::ButtonRole GemRequirementDialog::GetButtonResult()
+ {
+ return m_buttonResult;
+ }
+
+ void GemRequirementDialog::CancelButtonPressed()
+ {
+ m_buttonResult = QDialogButtonBox::RejectRole;
+ close();
+ }
+
+ void GemRequirementDialog::ContinueButtonPressed()
+ {
+ m_buttonResult = QDialogButtonBox::ApplyRole;
+ close();
+ }
+
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.h
new file mode 100644
index 0000000000..4295c5d586
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementDialog.h
@@ -0,0 +1,41 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include
+
+#include
+#endif
+
+namespace O3DE::ProjectManager
+{
+ QT_FORWARD_DECLARE_CLASS(GemModel)
+
+ class GemRequirementDialog
+ : public QDialog
+ {
+ Q_OBJECT // AUTOMOC
+ public:
+ explicit GemRequirementDialog(GemModel* model, const QVector& gemsToAdd, QWidget *parent = nullptr);
+ ~GemRequirementDialog() = default;
+
+ QDialogButtonBox::ButtonRole GetButtonResult();
+
+ private:
+ void CancelButtonPressed();
+ void ContinueButtonPressed();
+
+ QDialogButtonBox::ButtonRole m_buttonResult = QDialogButtonBox::RejectRole;
+ };
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.cpp
new file mode 100644
index 0000000000..120ec63313
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.cpp
@@ -0,0 +1,51 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include
+#include
+
+#include
+
+namespace O3DE::ProjectManager
+{
+ GemRequirementFilterProxyModel::GemRequirementFilterProxyModel(GemModel* sourceModel, const QVector& addedGems, QObject* parent)
+ : QSortFilterProxyModel(parent)
+ , m_sourceModel(sourceModel)
+ , m_addedGems(addedGems)
+ {
+ setSourceModel(sourceModel);
+ m_selectionProxyModel = new AzQtComponents::SelectionProxyModel(sourceModel->GetSelectionModel(), this, parent);
+ }
+
+ bool GemRequirementFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
+ {
+ // Do not use sourceParent->child because an invalid parent does not produce valid children (which our index function does)
+ QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent);
+ if (!sourceIndex.isValid())
+ {
+ return false;
+ }
+
+ if (!m_addedGems.contains(sourceIndex))
+ {
+ return false;
+ }
+
+ if (!m_sourceModel->HasRequirement(sourceIndex))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.h
new file mode 100644
index 0000000000..a891d63d0c
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementFilterProxyModel.h
@@ -0,0 +1,44 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#pragma once
+
+#if !defined(Q_MOC_RUN)
+#include
+#include
+#endif
+
+QT_FORWARD_DECLARE_CLASS(QItemSelectionModel)
+
+namespace O3DE::ProjectManager
+{
+ QT_FORWARD_DECLARE_CLASS(GemModel)
+
+ class GemRequirementFilterProxyModel
+ : public QSortFilterProxyModel
+ {
+ Q_OBJECT // AUTOMOC
+
+ public:
+ GemRequirementFilterProxyModel(GemModel* sourceModel, const QVector& addedGems, QObject* parent = nullptr);
+
+ AzQtComponents::SelectionProxyModel* GetSelectionModel() const { return m_selectionProxyModel; }
+
+ bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
+
+ private:
+ GemModel* m_sourceModel = nullptr;
+ AzQtComponents::SelectionProxyModel* m_selectionProxyModel = nullptr;
+
+ QVector m_addedGems;
+ };
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.cpp
new file mode 100644
index 0000000000..a86ae876c2
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.cpp
@@ -0,0 +1,30 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include
+#include
+#include
+
+namespace O3DE::ProjectManager
+{
+ GemRequirementListView::GemRequirementListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent)
+ : QListView(parent)
+ {
+ setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+
+ setStyleSheet("background-color: #444444;");
+
+ setModel(model);
+ setSelectionModel(selectionModel);
+ setItemDelegate(new GemRequirementDelegate(model, this));
+ }
+} // namespace O3DE::ProjectManager
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/StaticMeshMotionVector.azsl b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.h
similarity index 52%
rename from Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/StaticMeshMotionVector.azsl
rename to Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.h
index 11bc528f3b..25b2837e30 100644
--- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/StaticMeshMotionVector.azsl
+++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemRequirementListView.h
@@ -10,23 +10,23 @@
*
*/
-#include "./MeshMotionVectorCommon.azsli"
+#pragma once
-struct VSInput
+#if !defined(Q_MOC_RUN)
+#include
+#include
+#include
+#endif
+
+namespace O3DE::ProjectManager
{
- float3 m_position : POSITION;
-};
-
-VSOutput MainVS(VSInput IN)
-{
- VSOutput OUT;
-
- OUT.m_worldPos = mul(SceneSrg::GetObjectToWorldMatrix(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz;
- OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(OUT.m_worldPos, 1.0));
- OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz;
-
- return OUT;
-}
-
-
+ class GemRequirementListView
+ : public QListView
+ {
+ Q_OBJECT // AUTOMOC
+ public:
+ explicit GemRequirementListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr);
+ ~GemRequirementListView() = default;
+ };
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp
index 5faa6cb8bd..efe34f6fa6 100644
--- a/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp
+++ b/Code/Tools/ProjectManager/Source/NewProjectSettingsScreen.cpp
@@ -12,8 +12,8 @@
#include
#include
-#include
#include
+#include
#include
#include
#include
diff --git a/Code/Tools/ProjectManager/Source/ProjectBuilder.cpp b/Code/Tools/ProjectManager/Source/ProjectBuilder.cpp
index 8cdab93c6a..62f879963f 100644
--- a/Code/Tools/ProjectManager/Source/ProjectBuilder.cpp
+++ b/Code/Tools/ProjectManager/Source/ProjectBuilder.cpp
@@ -11,6 +11,7 @@
*/
#include
+#include
#include
#include
@@ -30,8 +31,6 @@ namespace O3DE::ProjectManager
{
// 10 Minutes
constexpr int MaxBuildTimeMSecs = 600000;
- static const QString BuildPathPostfix = "windows_vs2019";
- static const QString ErrorLogPathPostfix = "CMakeFiles/CMakeProjectBuildError.log";
ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo)
: QObject()
@@ -83,7 +82,7 @@ namespace O3DE::ProjectManager
QStringList
{
"-B",
- QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
+ QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix),
"-S",
m_projectInfo.m_path,
"-G",
@@ -123,7 +122,7 @@ namespace O3DE::ProjectManager
QStringList
{
"--build",
- QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
+ QDir(m_projectInfo.m_path).filePath(ProjectBuildPathPostfix),
"--target",
m_projectInfo.m_projectName + ".GameLauncher",
"Editor",
@@ -159,8 +158,8 @@ namespace O3DE::ProjectManager
QString ProjectBuilderWorker::LogFilePath() const
{
QDir logFilePath(m_projectInfo.m_path);
- logFilePath.cd(BuildPathPostfix);
- return logFilePath.filePath(ErrorLogPathPostfix);
+ logFilePath.cd(ProjectBuildPathPostfix);
+ return logFilePath.filePath(ProjectBuildErrorLogPathPostfix);
}
void ProjectBuilderWorker::WriteErrorLog(const QString& log)
diff --git a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp
index 3bde0a310d..aaf87fd9cf 100644
--- a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp
+++ b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp
@@ -11,6 +11,7 @@
*/
#include
+#include
#include
#include
@@ -22,12 +23,11 @@
#include
#include
#include
+#include
+#include
namespace O3DE::ProjectManager
{
- inline constexpr static int s_projectImageWidth = 210;
- inline constexpr static int s_projectImageHeight = 280;
-
LabelButton::LabelButton(QWidget* parent)
: QLabel(parent)
{
@@ -92,11 +92,6 @@ namespace O3DE::ProjectManager
: QFrame(parent)
, m_projectInfo(projectInfo)
{
- if (m_projectInfo.m_imagePath.isEmpty())
- {
- m_projectInfo.m_imagePath = ":/DefaultProjectImage.png";
- }
-
BaseSetup();
if (processing)
{
@@ -118,20 +113,25 @@ namespace O3DE::ProjectManager
setLayout(vLayout);
m_projectImageLabel = new LabelButton(this);
- m_projectImageLabel->setFixedSize(s_projectImageWidth, s_projectImageHeight);
+ m_projectImageLabel->setFixedSize(ProjectPreviewImageWidth, ProjectPreviewImageHeight);
m_projectImageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
vLayout->addWidget(m_projectImageLabel);
- m_projectImageLabel->setPixmap(
- QPixmap(m_projectInfo.m_imagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding));
+ QString projectPreviewPath = QDir(m_projectInfo.m_path).filePath(m_projectInfo.m_iconPath);
+ QFileInfo doesPreviewExist(projectPreviewPath);
+ if (!doesPreviewExist.exists() || !doesPreviewExist.isFile())
+ {
+ projectPreviewPath = ":/DefaultProjectImage.png";
+ }
+ m_projectImageLabel->setPixmap(QPixmap(projectPreviewPath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding));
m_projectFooter = new QFrame(this);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setContentsMargins(0, 0, 0, 0);
m_projectFooter->setLayout(hLayout);
{
- QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this);
+ QLabel* projectNameLabel = new QLabel(m_projectInfo.GetProjectDisplayName(), this);
hLayout->addWidget(projectNameLabel);
}
diff --git a/Code/Tools/ProjectManager/Source/ProjectInfo.cpp b/Code/Tools/ProjectManager/Source/ProjectInfo.cpp
index 99649cbfdf..e7d8a6d0e7 100644
--- a/Code/Tools/ProjectManager/Source/ProjectInfo.cpp
+++ b/Code/Tools/ProjectManager/Source/ProjectInfo.cpp
@@ -10,33 +10,74 @@
*
*/
-#include "ProjectInfo.h"
+#include
+#include
+
+#include
namespace O3DE::ProjectManager
{
- ProjectInfo::ProjectInfo(const QString& path, const QString& projectName, const QString& displayName,
- const QString& origin, const QString& summary, const QString& imagePath, const QString& backgroundImagePath,
+ ProjectInfo::ProjectInfo(
+ const QString& path,
+ const QString& projectName,
+ const QString& displayName,
+ const QString& origin,
+ const QString& summary,
+ const QString& iconPath,
+ const QString& newPreviewImagePath,
+ const QString& newBackgroundImagePath,
bool needsBuild)
: m_path(path)
, m_projectName(projectName)
, m_displayName(displayName)
, m_origin(origin)
, m_summary(summary)
- , m_imagePath(imagePath)
- , m_backgroundImagePath(backgroundImagePath)
+ , m_iconPath(iconPath)
+ , m_newPreviewImagePath(newPreviewImagePath)
+ , m_newBackgroundImagePath(newBackgroundImagePath)
, m_needsBuild(needsBuild)
{
}
- bool ProjectInfo::operator==(const ProjectInfo& rhs)
+ bool ProjectInfo::operator==(const ProjectInfo& rhs) const
{
- return m_path == rhs.m_path
- && m_projectName == rhs.m_projectName
- && m_imagePath == rhs.m_imagePath
- && m_backgroundImagePath == rhs.m_backgroundImagePath;
+ if (m_path != rhs.m_path)
+ {
+ return false;
+ }
+ if (m_projectName != rhs.m_projectName)
+ {
+ return false;
+ }
+ if (m_displayName != rhs.m_displayName)
+ {
+ return false;
+ }
+ if (m_origin != rhs.m_origin)
+ {
+ return false;
+ }
+ if (m_summary != rhs.m_summary)
+ {
+ return false;
+ }
+ if (m_iconPath != rhs.m_iconPath)
+ {
+ return false;
+ }
+ if (m_newPreviewImagePath != rhs.m_newPreviewImagePath)
+ {
+ return false;
+ }
+ if (m_newBackgroundImagePath != rhs.m_newBackgroundImagePath)
+ {
+ return false;
+ }
+
+ return true;
}
- bool ProjectInfo::operator!=(const ProjectInfo& rhs)
+ bool ProjectInfo::operator!=(const ProjectInfo& rhs) const
{
return !operator==(rhs);
}
@@ -45,4 +86,16 @@ namespace O3DE::ProjectManager
{
return !m_path.isEmpty() && !m_projectName.isEmpty();
}
+
+ const QString& ProjectInfo::GetProjectDisplayName() const
+ {
+ if (!m_displayName.isEmpty())
+ {
+ return m_displayName;
+ }
+ else
+ {
+ return m_projectName;
+ }
+ }
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/ProjectInfo.h b/Code/Tools/ProjectManager/Source/ProjectInfo.h
index 184916a514..72f64408f4 100644
--- a/Code/Tools/ProjectManager/Source/ProjectInfo.h
+++ b/Code/Tools/ProjectManager/Source/ProjectInfo.h
@@ -31,14 +31,16 @@ namespace O3DE::ProjectManager
const QString& displayName,
const QString& origin,
const QString& summary,
- const QString& imagePath,
- const QString& backgroundImagePath,
+ const QString& iconPath,
+ const QString& newPreviewImagePath,
+ const QString& newBackgroundImagePath,
bool needsBuild);
- bool operator==(const ProjectInfo& rhs);
- bool operator!=(const ProjectInfo& rhs);
+ bool operator==(const ProjectInfo& rhs) const;
+ bool operator!=(const ProjectInfo& rhs) const;
bool IsValid() const;
+ const QString& GetProjectDisplayName() const;
// from o3de_manifest.json and o3de_projects.json
QString m_path;
@@ -48,14 +50,14 @@ namespace O3DE::ProjectManager
QString m_displayName;
QString m_origin;
QString m_summary;
+ QString m_iconPath;
QStringList m_userTags;
- // Used on projects home screen
- QString m_imagePath;
- QString m_backgroundImagePath;
+ // Used as temp variable for replace images
+ QString m_newPreviewImagePath;
+ QString m_newBackgroundImagePath;
// Used in project creation
-
bool m_needsBuild = false; //! Does this project need to be built
};
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerDefs.h b/Code/Tools/ProjectManager/Source/ProjectManagerDefs.h
new file mode 100644
index 0000000000..eafab28a60
--- /dev/null
+++ b/Code/Tools/ProjectManager/Source/ProjectManagerDefs.h
@@ -0,0 +1,24 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+#pragma once
+
+#include
+
+namespace O3DE::ProjectManager
+{
+ inline constexpr static int ProjectPreviewImageWidth = 210;
+ inline constexpr static int ProjectPreviewImageHeight = 280;
+
+ static const QString ProjectBuildPathPostfix = "Windows_VS2019";
+ static const QString ProjectBuildErrorLogPathPostfix = "CMakeFiles/CMakeProjectBuildError.log";
+ static const QString ProjectPreviewImagePath = "preview.png";
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp
index b198724353..9dbbf26aa4 100644
--- a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp
+++ b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp
@@ -11,7 +11,7 @@
*/
#include
-#include
+#include
#include
#include
#include
@@ -47,7 +47,7 @@ namespace O3DE::ProjectManager
connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::ValidateProjectName);
m_verticalLayout->addWidget(m_projectName);
- m_projectPath = new FormBrowseEditWidget(tr("Project Location"), "", this);
+ m_projectPath = new FormFolderBrowseEditWidget(tr("Project Location"), "", this);
m_projectPath->lineEdit()->setReadOnly(true);
connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::Validate);
m_verticalLayout->addWidget(m_projectPath);
diff --git a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.h b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.h
index 0d75bbbc64..1b1f03051d 100644
--- a/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.h
+++ b/Code/Tools/ProjectManager/Source/ProjectSettingsScreen.h
@@ -32,9 +32,9 @@ namespace O3DE::ProjectManager
~ProjectSettingsScreen() = default;
ProjectManagerScreen GetScreenEnum() override;
- ProjectInfo GetProjectInfo();
+ virtual ProjectInfo GetProjectInfo();
- bool Validate();
+ virtual bool Validate();
protected slots:
virtual bool ValidateProjectName();
diff --git a/Code/Tools/ProjectManager/Source/ProjectUtils.cpp b/Code/Tools/ProjectManager/Source/ProjectUtils.cpp
index 3e2b3c13e1..91e7f0a719 100644
--- a/Code/Tools/ProjectManager/Source/ProjectUtils.cpp
+++ b/Code/Tools/ProjectManager/Source/ProjectUtils.cpp
@@ -29,11 +29,8 @@ namespace O3DE::ProjectManager
if (!QDir(path).isEmpty())
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(
- parent,
- QObject::tr("Overwrite Directory"),
- QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"),
- QMessageBox::No | QMessageBox::Yes
- );
+ parent, QObject::tr("Overwrite Directory"),
+ QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"), QMessageBox::No | QMessageBox::Yes);
if (warningResult != QMessageBox::Yes)
{
@@ -53,14 +50,13 @@ namespace O3DE::ProjectManager
{
if (ancestor == descendent)
{
- return false;
+ return true;
}
descendent.cdUp();
- }
- while (!descendent.isRoot());
+ } while (!descendent.isRoot());
- return true;
+ return false;
}
static bool CopyDirectory(const QString& origPath, const QString& newPath)
@@ -138,7 +134,7 @@ namespace O3DE::ProjectManager
bool CopyProject(const QString& origPath, const QString& newPath)
{
// Disallow copying from or into subdirectory
- if (!IsDirectoryDescedent(origPath, newPath) || !IsDirectoryDescedent(newPath, origPath))
+ if (IsDirectoryDescedent(origPath, newPath) || IsDirectoryDescedent(newPath, origPath))
{
return false;
}
@@ -173,20 +169,66 @@ namespace O3DE::ProjectManager
return false;
}
- bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent)
+ bool MoveProject(QString origPath, QString newPath, QWidget* parent, bool ignoreRegister)
{
- if (!WarnDirectoryOverwrite(newPath, parent) || !UnregisterProject(origPath))
+ origPath = QDir::toNativeSeparators(origPath);
+ newPath = QDir::toNativeSeparators(newPath);
+
+ if (!WarnDirectoryOverwrite(newPath, parent) || (!ignoreRegister && !UnregisterProject(origPath)))
{
return false;
}
- QDir directory;
- if (directory.rename(origPath, newPath))
+ QDir newDirectory(newPath);
+ if (!newDirectory.removeRecursively())
{
- return directory.rename(origPath, newPath);
+ return false;
+ }
+ if (!newDirectory.rename(origPath, newPath))
+ {
+ // Likely failed because trying to move to another partition, try copying
+ if (!CopyProject(origPath, newPath))
+ {
+ return false;
+ }
+
+ DeleteProjectFiles(origPath, true);
}
- if (!RegisterProject(newPath))
+ if (!ignoreRegister && !RegisterProject(newPath))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ bool ReplaceFile(const QString& origFile, const QString& newFile, QWidget* parent, bool interactive)
+ {
+ QFileInfo original(origFile);
+ if (original.exists())
+ {
+ if (interactive)
+ {
+ QMessageBox::StandardButton warningResult = QMessageBox::warning(
+ parent,
+ QObject::tr("Overwrite File?"),
+ QObject::tr("Replacing this will overwrite the current file on disk. Are you sure?"),
+ QMessageBox::No | QMessageBox::Yes);
+
+ if (warningResult == QMessageBox::No)
+ {
+ return false;
+ }
+ }
+
+ if (!QFile::remove(origFile))
+ {
+ return false;
+ }
+ }
+
+ if (!QFile::copy(newFile, origFile))
{
return false;
}
diff --git a/Code/Tools/ProjectManager/Source/ProjectUtils.h b/Code/Tools/ProjectManager/Source/ProjectUtils.h
index 9c711ad187..2fa1258e8f 100644
--- a/Code/Tools/ProjectManager/Source/ProjectUtils.h
+++ b/Code/Tools/ProjectManager/Source/ProjectUtils.h
@@ -24,7 +24,9 @@ namespace O3DE::ProjectManager
bool CopyProjectDialog(const QString& origPath, QWidget* parent = nullptr);
bool CopyProject(const QString& origPath, const QString& newPath);
bool DeleteProjectFiles(const QString& path, bool force = false);
- bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr);
+ bool MoveProject(QString origPath, QString newPath, QWidget* parent = nullptr, bool ignoreRegister = false);
+
+ bool ReplaceFile(const QString& origFile, const QString& newFile, QWidget* parent = nullptr, bool interactive = true);
bool IsVS2019Installed();
diff --git a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp
index 6633558406..d0534c1e76 100644
--- a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp
+++ b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp
@@ -12,6 +12,7 @@
#include
+#include
#include
#include
#include
@@ -35,7 +36,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -218,16 +218,7 @@ namespace O3DE::ProjectManager
ProjectButton* ProjectsScreen::CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing)
{
- ProjectButton* projectButton;
-
- QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
- QFileInfo doesPreviewExist(projectPreviewPath);
- if (doesPreviewExist.exists() && doesPreviewExist.isFile())
- {
- project.m_imagePath = projectPreviewPath;
- }
-
- projectButton = new ProjectButton(project, this, processing);
+ ProjectButton* projectButton = new ProjectButton(project, this, processing);
flowLayout->addWidget(projectButton);
@@ -438,7 +429,7 @@ namespace O3DE::ProjectManager
{
QMessageBox::information(this,
tr("Project Should be rebuilt."),
- projectInfo.m_projectName + tr(" project likely needs to be rebuilt."));
+ projectInfo.GetProjectDisplayName() + tr(" project likely needs to be rebuilt."));
}
}
@@ -499,8 +490,8 @@ namespace O3DE::ProjectManager
{
QMessageBox::StandardButton buildProject = QMessageBox::information(
this,
- tr("Building \"%1\"").arg(projectInfo.m_projectName),
- tr("Ready to build \"%1\"?").arg(projectInfo.m_projectName),
+ tr("Building \"%1\"").arg(projectInfo.GetProjectDisplayName()),
+ tr("Ready to build \"%1\"?").arg(projectInfo.GetProjectDisplayName()),
QMessageBox::No | QMessageBox::Yes);
if (buildProject == QMessageBox::Yes)
diff --git a/Code/Tools/ProjectManager/Source/ProjectsScreen.h b/Code/Tools/ProjectManager/Source/ProjectsScreen.h
index bc28d4ef30..b86c2b0240 100644
--- a/Code/Tools/ProjectManager/Source/ProjectsScreen.h
+++ b/Code/Tools/ProjectManager/Source/ProjectsScreen.h
@@ -80,8 +80,6 @@ namespace O3DE::ProjectManager
QQueue m_buildQueue;
ProjectBuilderController* m_currentBuilder = nullptr;
- const QString m_projectPreviewImagePath = "/preview.png";
-
inline constexpr static int s_contentMargins = 80;
inline constexpr static int s_spacerSize = 20;
};
diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp
index 0e00319b6b..db376fb195 100644
--- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp
+++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp
@@ -12,6 +12,7 @@
#include
+#include
// Qt defines slots, which interferes with the use here.
#pragma push_macro("slots")
@@ -660,6 +661,7 @@ namespace O3DE::ProjectManager
gemInfo.m_displayName = Py_To_String_Optional(data, "DisplayName", gemInfo.m_name);
gemInfo.m_summary = Py_To_String_Optional(data, "Summary", "");
gemInfo.m_version = Py_To_String_Optional(data, "Version", "");
+ gemInfo.m_requirement = Py_To_String_Optional(data, "Requirements", "");
if (data.contains("Tags"))
{
@@ -693,6 +695,7 @@ namespace O3DE::ProjectManager
projectInfo.m_displayName = Py_To_String_Optional(projectData, "display_name", projectInfo.m_projectName);
projectInfo.m_origin = Py_To_String_Optional(projectData, "origin", projectInfo.m_origin);
projectInfo.m_summary = Py_To_String_Optional(projectData, "summary", projectInfo.m_summary);
+ projectInfo.m_iconPath = Py_To_String_Optional(projectData, "icon", ProjectPreviewImagePath);
if (projectData.contains("user_tags"))
{
for (auto tag : projectData["user_tags"])
@@ -786,7 +789,7 @@ namespace O3DE::ProjectManager
pybind11::str(projectInfo.m_origin.toStdString()), // new_origin
pybind11::str(projectInfo.m_displayName.toStdString()), // new_display
pybind11::str(projectInfo.m_summary.toStdString()), // new_summary
- pybind11::str(projectInfo.m_imagePath.toStdString()), // new_icon
+ pybind11::str(projectInfo.m_iconPath.toStdString()), // new_icon
pybind11::none(), // add_tags not used
pybind11::none(), // remove_tags not used
pybind11::list(pybind11::cast(newTags))); // replace_tags
diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp
index 409c51315d..e51d9e4996 100644
--- a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp
+++ b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp
@@ -11,6 +11,7 @@
*/
#include
+#include
#include
#include
#include
@@ -24,6 +25,7 @@
#include
#include
#include
+#include
namespace O3DE::ProjectManager
{
@@ -101,8 +103,11 @@ namespace O3DE::ProjectManager
void UpdateProjectCtrl::HandleGemsButton()
{
- m_stack->setCurrentWidget(m_gemCatalogScreen);
- Update();
+ if (UpdateProjectSettings(true))
+ {
+ m_stack->setCurrentWidget(m_gemCatalogScreen);
+ Update();
+ }
}
void UpdateProjectCtrl::HandleBackButton()
@@ -114,7 +119,10 @@ namespace O3DE::ProjectManager
}
else
{
- emit GotoPreviousScreenRequest();
+ if (UpdateProjectSettings(true))
+ {
+ emit GotoPreviousScreenRequest();
+ }
}
}
@@ -124,44 +132,19 @@ namespace O3DE::ProjectManager
if (m_stack->currentIndex() == ScreenOrder::Settings && m_updateSettingsScreen)
{
- if (m_updateSettingsScreen)
+ if (!UpdateProjectSettings())
{
- if (!m_updateSettingsScreen->Validate())
- {
- QMessageBox::critical(this, tr("Invalid project settings"), tr("Invalid project settings"));
- return;
- }
-
- ProjectInfo newProjectSettings = m_updateSettingsScreen->GetProjectInfo();
-
- // Update project if settings changed
- if (m_projectInfo != newProjectSettings)
- {
- auto result = PythonBindingsInterface::Get()->UpdateProject(newProjectSettings);
- if (!result.IsSuccess())
- {
- QMessageBox::critical(this, tr("Project update failed"), tr(result.GetError().c_str()));
- return;
- }
- }
-
- // Check if project path has changed and move it
- if (newProjectSettings.m_path != m_projectInfo.m_path)
- {
- if (!ProjectUtils::MoveProject(m_projectInfo.m_path, newProjectSettings.m_path))
- {
- QMessageBox::critical(this, tr("Project move failed"), tr("Failed to move project."));
- return;
- }
- }
-
- m_projectInfo = newProjectSettings;
+ return;
}
}
else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
{
// Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
- m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path);
+ if (!m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path))
+ {
+ QMessageBox::critical(this, tr("Failed to configure gems"), tr("Failed to configure gems for project."));
+ return;
+ }
shouldRebuild = true;
}
@@ -190,14 +173,15 @@ namespace O3DE::ProjectManager
{
if (m_stack->currentIndex() == ScreenOrder::Gems)
{
- m_header->setTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName));
+
+ m_header->setTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.GetProjectDisplayName()));
m_header->setSubTitle(QString(tr("Configure Gems")));
- m_nextButton->setText(tr("Finalize"));
+ m_nextButton->setText(tr("Save"));
}
else
{
m_header->setTitle("");
- m_header->setSubTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.m_projectName));
+ m_header->setSubTitle(QString(tr("Edit Project Settings: \"%1\"")).arg(m_projectInfo.GetProjectDisplayName()));
m_nextButton->setText(tr("Save"));
}
}
@@ -207,4 +191,70 @@ namespace O3DE::ProjectManager
m_updateSettingsScreen->SetProjectInfo(m_projectInfo);
}
+ bool UpdateProjectCtrl::UpdateProjectSettings(bool shouldConfirm)
+ {
+ AZ_Assert(m_updateSettingsScreen, "Update settings screen is nullptr.")
+
+ ProjectInfo newProjectSettings = m_updateSettingsScreen->GetProjectInfo();
+
+ if (m_projectInfo != newProjectSettings)
+ {
+ if (shouldConfirm)
+ {
+ QMessageBox::StandardButton warningResult = QMessageBox::warning(
+ this,
+ QObject::tr("Unsaved Changes!"),
+ QObject::tr("Would you like to save your changes to project settings?"),
+ QMessageBox::No | QMessageBox::Yes
+ );
+
+ if (warningResult == QMessageBox::No)
+ {
+ return true;
+ }
+ }
+
+ if (!m_updateSettingsScreen->Validate())
+ {
+ QMessageBox::critical(this, tr("Invalid project settings"), tr("Invalid project settings"));
+ return false;
+ }
+
+ // Update project if settings changed
+ {
+ auto result = PythonBindingsInterface::Get()->UpdateProject(newProjectSettings);
+ if (!result.IsSuccess())
+ {
+ QMessageBox::critical(this, tr("Project update failed"), tr(result.GetError().c_str()));
+ return false;
+ }
+ }
+
+ // Check if project path has changed and move it
+ if (newProjectSettings.m_path != m_projectInfo.m_path)
+ {
+ if (!ProjectUtils::MoveProject(m_projectInfo.m_path, newProjectSettings.m_path))
+ {
+ QMessageBox::critical(this, tr("Project move failed"), tr("Failed to move project."));
+ return false;
+ }
+ }
+
+ if (!newProjectSettings.m_newPreviewImagePath.isEmpty())
+ {
+ if (!ProjectUtils::ReplaceFile(
+ QDir(newProjectSettings.m_path).filePath(newProjectSettings.m_iconPath), newProjectSettings.m_newPreviewImagePath))
+ {
+ QMessageBox::critical(this, tr("File replace failed"), tr("Failed to replace project preview image."));
+ return false;
+ }
+ m_updateSettingsScreen->ResetProjectPreviewPath();
+ }
+
+ m_projectInfo = newProjectSettings;
+ }
+
+ return true;
+ }
+
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.h b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.h
index 231bfb8f19..b8b57c0c20 100644
--- a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.h
+++ b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.h
@@ -46,6 +46,7 @@ namespace O3DE::ProjectManager
private:
void Update();
void UpdateSettingsScreen();
+ bool UpdateProjectSettings(bool shouldConfirm = false);
enum ScreenOrder
{
diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp
index c29be3c7fd..f7be7e84c3 100644
--- a/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp
+++ b/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp
@@ -11,17 +11,43 @@
*/
#include
-#include
+#include
+#include
#include
+#include
#include
#include
+#include
+#include
namespace O3DE::ProjectManager
{
UpdateProjectSettingsScreen::UpdateProjectSettingsScreen(QWidget* parent)
: ProjectSettingsScreen(parent)
+ , m_userChangedPreview(false)
{
+ m_projectPreview = new FormImageBrowseEditWidget(tr("Project Preview"), "", this);
+ m_projectPreview->lineEdit()->setReadOnly(true);
+ connect(m_projectPreview->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::Validate);
+ connect(m_projectPreview->lineEdit(), &QLineEdit::textChanged, this, &UpdateProjectSettingsScreen::PreviewPathChanged);
+ connect(m_projectPath->lineEdit(), &QLineEdit::textChanged, this, &UpdateProjectSettingsScreen::UpdateProjectPreviewPath);
+ m_verticalLayout->addWidget(m_projectPreview);
+
+ QVBoxLayout* previewExtrasLayout = new QVBoxLayout(this);
+ previewExtrasLayout->setAlignment(Qt::AlignLeft);
+ previewExtrasLayout->setContentsMargins(50, 0, 0, 0);
+
+ QLabel* projectPreviewLabel = new QLabel(tr("Select an image (PNG). Minimum %1 x %2 pixels.")
+ .arg(QString::number(ProjectPreviewImageWidth), QString::number(ProjectPreviewImageHeight)));
+ previewExtrasLayout->addWidget(projectPreviewLabel);
+
+ m_projectPreviewImage = new QLabel(this);
+ m_projectPreviewImage->setFixedSize(ProjectPreviewImageWidth, ProjectPreviewImageHeight);
+ m_projectPreviewImage->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
+ previewExtrasLayout->addWidget(m_projectPreviewImage);
+
+ m_verticalLayout->addLayout(previewExtrasLayout);
}
ProjectManagerScreen UpdateProjectSettingsScreen::GetScreenEnum()
@@ -29,10 +55,58 @@ namespace O3DE::ProjectManager
return ProjectManagerScreen::UpdateProjectSettings;
}
+ ProjectInfo UpdateProjectSettingsScreen::GetProjectInfo()
+ {
+ m_projectInfo.m_displayName = m_projectName->lineEdit()->text();
+ m_projectInfo.m_path = m_projectPath->lineEdit()->text();
+
+ if (m_userChangedPreview)
+ {
+ m_projectInfo.m_iconPath = ProjectPreviewImagePath;
+ m_projectInfo.m_newPreviewImagePath = m_projectPreview->lineEdit()->text();
+ }
+ return m_projectInfo;
+ }
+
void UpdateProjectSettingsScreen::SetProjectInfo(const ProjectInfo& projectInfo)
{
- m_projectName->lineEdit()->setText(projectInfo.m_projectName);
+ m_projectInfo = projectInfo;
+
+ m_projectName->lineEdit()->setText(projectInfo.GetProjectDisplayName());
+
m_projectPath->lineEdit()->setText(projectInfo.m_path);
+ UpdateProjectPreviewPath();
+ }
+
+ void UpdateProjectSettingsScreen::UpdateProjectPreviewPath()
+ {
+ if (!m_userChangedPreview)
+ {
+ m_projectPreview->lineEdit()->setText(QDir(m_projectPath->lineEdit()->text()).filePath(m_projectInfo.m_iconPath));
+ // Setting the text sets m_userChangedPreview to true
+ // Set it back to false because it should only be true when changed by user
+ m_userChangedPreview = false;
+ }
+ }
+
+ bool UpdateProjectSettingsScreen::Validate()
+ {
+ return ProjectSettingsScreen::Validate() && ValidateProjectPreview();
+ }
+
+ void UpdateProjectSettingsScreen::ResetProjectPreviewPath()
+ {
+ m_userChangedPreview = false;
+ UpdateProjectPreviewPath();
+ }
+
+ void UpdateProjectSettingsScreen::PreviewPathChanged()
+ {
+ m_userChangedPreview = true;
+
+ // Update with latest image
+ m_projectPreviewImage->setPixmap(
+ QPixmap(m_projectPreview->lineEdit()->text()).scaled(m_projectPreviewImage->size(), Qt::KeepAspectRatioByExpanding));
}
bool UpdateProjectSettingsScreen::ValidateProjectPath()
@@ -48,4 +122,39 @@ namespace O3DE::ProjectManager
return projectPathIsValid;
}
+ bool UpdateProjectSettingsScreen::ValidateProjectPreview()
+ {
+ bool projectPreviewIsValid = true;
+
+ if (m_projectPreview->lineEdit()->text().isEmpty())
+ {
+ projectPreviewIsValid = false;
+ m_projectPreview->setErrorLabelText(tr("Please select a file."));
+ }
+ else
+ {
+ if (m_userChangedPreview)
+ {
+ QFileInfo previewFile(m_projectPreview->lineEdit()->text());
+ if (!previewFile.exists() || !previewFile.isFile())
+ {
+ projectPreviewIsValid = false;
+ m_projectPreview->setErrorLabelText(tr("Please select a valid png file."));
+ }
+ else
+ {
+ QString fileType = previewFile.completeSuffix().toLower();
+ if (fileType != "png")
+ {
+ projectPreviewIsValid = false;
+ m_projectPreview->setErrorLabelText(tr("Please select a png image."));
+ }
+ }
+ }
+ }
+
+ m_projectPreview->setErrorLabelVisible(!projectPreviewIsValid);
+ return projectPreviewIsValid;
+ }
+
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.h b/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.h
index 95bbceb9c6..1961648b5b 100644
--- a/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.h
+++ b/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.h
@@ -15,6 +15,8 @@
#include
#endif
+QT_FORWARD_DECLARE_CLASS(QLabel)
+
namespace O3DE::ProjectManager
{
class UpdateProjectSettingsScreen
@@ -25,10 +27,26 @@ namespace O3DE::ProjectManager
~UpdateProjectSettingsScreen() = default;
ProjectManagerScreen GetScreenEnum() override;
+ ProjectInfo GetProjectInfo() override;
void SetProjectInfo(const ProjectInfo& projectInfo);
+ bool Validate() override;
+
+ void ResetProjectPreviewPath();
+
+ public slots:
+ void UpdateProjectPreviewPath();
+ void PreviewPathChanged();
+
protected:
bool ValidateProjectPath() override;
+ virtual bool ValidateProjectPreview();
+
+ FormBrowseEditWidget* m_projectPreview;
+ QLabel* m_projectPreviewImage;
+
+ ProjectInfo m_projectInfo;
+ bool m_userChangedPreview; //! Did the user change the project preview path
};
} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake
index 587b5907bb..7eaeed0b96 100644
--- a/Code/Tools/ProjectManager/project_manager_files.cmake
+++ b/Code/Tools/ProjectManager/project_manager_files.cmake
@@ -13,6 +13,7 @@
set(FILES
Source/Application.h
Source/Application.cpp
+ Source/ProjectManagerDefs.h
Source/ScreenDefs.h
Source/ScreenFactory.h
Source/ScreenFactory.cpp
@@ -25,6 +26,10 @@ set(FILES
Source/FormLineEditWidget.cpp
Source/FormBrowseEditWidget.h
Source/FormBrowseEditWidget.cpp
+ Source/FormFolderBrowseEditWidget.h
+ Source/FormFolderBrowseEditWidget.cpp
+ Source/FormImageBrowseEditWidget.h
+ Source/FormImageBrowseEditWidget.cpp
Source/PathValidator.h
Source/PathValidator.cpp
Source/ProjectManagerWindow.h
@@ -82,6 +87,14 @@ set(FILES
Source/GemCatalog/GemListHeaderWidget.cpp
Source/GemCatalog/GemModel.h
Source/GemCatalog/GemModel.cpp
+ Source/GemCatalog/GemRequirementDialog.h
+ Source/GemCatalog/GemRequirementDialog.cpp
+ Source/GemCatalog/GemRequirementDelegate.h
+ Source/GemCatalog/GemRequirementDelegate.cpp
+ Source/GemCatalog/GemRequirementFilterProxyModel.h
+ Source/GemCatalog/GemRequirementFilterProxyModel.cpp
+ Source/GemCatalog/GemRequirementListView.h
+ Source/GemCatalog/GemRequirementListView.cpp
Source/GemCatalog/GemSortFilterProxyModel.h
Source/GemCatalog/GemSortFilterProxyModel.cpp
)
diff --git a/Code/Tools/ProjectManager/project_manager_tests_files.cmake b/Code/Tools/ProjectManager/project_manager_tests_files.cmake
index e1e84a43a7..e340469bcc 100644
--- a/Code/Tools/ProjectManager/project_manager_tests_files.cmake
+++ b/Code/Tools/ProjectManager/project_manager_tests_files.cmake
@@ -14,4 +14,5 @@ set(FILES
Resources/ProjectManager.qss
tests/ApplicationTests.cpp
tests/main.cpp
+ tests/UtilsTests.cpp
)
diff --git a/Code/Tools/ProjectManager/tests/UtilsTests.cpp b/Code/Tools/ProjectManager/tests/UtilsTests.cpp
new file mode 100644
index 0000000000..e248d37bee
--- /dev/null
+++ b/Code/Tools/ProjectManager/tests/UtilsTests.cpp
@@ -0,0 +1,120 @@
+/*
+ * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+ * its licensors.
+ *
+ * For complete copyright and license terms please see the LICENSE at the root of this
+ * distribution (the "License"). All use of this software is governed by the License,
+ * or, if provided, by the license below or the license accompanying this file. Do not
+ * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ */
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace O3DE::ProjectManager
+{
+ namespace ProjectUtils
+ {
+ class ProjectManagerUtilsTests
+ : public ::UnitTest::ScopedAllocatorSetupFixture
+ {
+ public:
+ ProjectManagerUtilsTests()
+ {
+ m_application = AZStd::make_unique();
+ m_application->Init(false);
+
+ QDir dir;
+ dir.mkdir("ProjectA");
+ dir.mkdir("ProjectB");
+
+ QFile origFile("ProjectA/origFile.txt");
+ if (origFile.open(QIODevice::ReadWrite))
+ {
+ QTextStream stream(&origFile);
+ stream << "orig" << Qt::endl;
+ origFile.close();
+ }
+
+ QFile replaceFile("ProjectA/replaceFile.txt");
+ if (replaceFile.open(QIODevice::ReadWrite))
+ {
+ QTextStream stream(&replaceFile);
+ stream << "replace" << Qt::endl;
+ replaceFile.close();
+ }
+ }
+
+ ~ProjectManagerUtilsTests()
+ {
+ QDir dirA("ProjectA");
+ dirA.removeRecursively();
+
+ QDir dirB("ProjectB");
+ dirB.removeRecursively();
+
+ m_application.reset();
+ }
+
+ AZStd::unique_ptr m_application;
+ };
+
+#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
+ TEST_F(ProjectManagerUtilsTests, DISABLED_MoveProject_Succeeds)
+#else
+ TEST_F(ProjectManagerUtilsTests, MoveProject_Succeeds)
+#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
+ {
+ EXPECT_TRUE(MoveProject(
+ QDir::currentPath() + QDir::separator() + "ProjectA",
+ QDir::currentPath() + QDir::separator() + "ProjectB",
+ nullptr, true));
+
+ QFileInfo origFile("ProjectA/origFile.txt");
+ EXPECT_TRUE(!origFile.exists());
+
+ QFileInfo replaceFile("ProjectA/replaceFile.txt");
+ EXPECT_TRUE(!replaceFile.exists());
+
+ QFileInfo origFileMoved("ProjectB/origFile.txt");
+ EXPECT_TRUE(origFileMoved.exists() && origFileMoved.isFile());
+
+ QFileInfo replaceFileMoved("ProjectB/replaceFile.txt");
+ EXPECT_TRUE(replaceFileMoved.exists() && replaceFileMoved.isFile());
+ }
+
+#if AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
+ TEST_F(ProjectManagerUtilsTests, DISABLED_ReplaceFile_Succeeds)
+#else
+ TEST_F(ProjectManagerUtilsTests, ReplaceFile_Succeeds)
+#endif // !AZ_TRAIT_DISABLE_FAILED_PROJECT_MANAGER_TESTS
+ {
+ EXPECT_TRUE(ReplaceFile("ProjectA/origFile.txt", "ProjectA/replaceFile.txt", nullptr, false));
+
+ QFile origFile("ProjectA/origFile.txt");
+ if (origFile.open(QIODevice::ReadOnly))
+ {
+ QTextStream stream(&origFile);
+ QString line = stream.readLine();
+ EXPECT_EQ(line, "replace");
+
+ origFile.close();
+ }
+ else
+ {
+ FAIL();
+ }
+ }
+ } // namespace ProjectUtils
+} // namespace O3DE::ProjectManager
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp
index 25b58ab544..b81d013b87 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpAnimationImporter.cpp
@@ -255,7 +255,6 @@ namespace AZ
{
return AZStd::make_pair(animation, anim);
}
-
Events::ProcessingResult AssImpAnimationImporter::ImportAnimation(AssImpSceneNodeAppendedContext& context)
{
AZ_TraceContext("Importer", "Animation");
@@ -447,7 +446,22 @@ namespace AZ
return combinedAnimationResult.GetResult();
}
- decltype(boneAnimations) parentFillerAnimations;
+
+ AZStd::unordered_set boneList;
+
+ for (int meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
+ {
+ aiMesh* mesh = scene->mMeshes[meshIndex];
+
+ for (int boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
+ {
+ aiBone* bone = mesh->mBones[boneIndex];
+
+ boneList.insert(bone->mName.C_Str());
+ }
+ }
+
+ decltype(boneAnimations) fillerAnimations;
// Go through all the animations and make sure we create animations for bones who's parents don't have an animation
for (auto&& anim : boneAnimations)
@@ -459,8 +473,8 @@ namespace AZ
{
if (!IsPivotNode(parent->mName))
{
- if (boneAnimations.find(parent->mName.C_Str()) == boneAnimations.end() &&
- parentFillerAnimations.find(parent->mName.C_Str()) == parentFillerAnimations.end())
+ if (!boneAnimations.contains(parent->mName.C_Str()) &&
+ !fillerAnimations.contains(parent->mName.C_Str()))
{
// Create 1 key for each type that just copies the current transform
ConsolidatedNodeAnim emptyAnimation;
@@ -472,7 +486,7 @@ namespace AZ
globalTransform.Decompose(scale, rotation, position);
emptyAnimation.mNumRotationKeys = emptyAnimation.mNumPositionKeys = emptyAnimation.mNumScalingKeys = 1;
-
+
emptyAnimation.m_ownedPositionKeys.emplace_back(0, position);
emptyAnimation.mPositionKeys = emptyAnimation.m_ownedPositionKeys.data();
@@ -481,9 +495,9 @@ namespace AZ
emptyAnimation.m_ownedScalingKeys.emplace_back(0, scale);
emptyAnimation.mScalingKeys = emptyAnimation.m_ownedScalingKeys.data();
-
- parentFillerAnimations.insert(
- AZStd::make_pair(parent->mName.C_Str(), AZStd::make_pair(anim.second.first, AZStd::move(emptyAnimation))));
+
+ fillerAnimations.insert(AZStd::make_pair(
+ parent->mName.C_Str(), AZStd::make_pair(anim.second.first, AZStd::move(emptyAnimation))));
}
}
@@ -491,7 +505,7 @@ namespace AZ
}
}
- boneAnimations.insert(AZStd::make_move_iterator(parentFillerAnimations.begin()), AZStd::make_move_iterator(parentFillerAnimations.end()));
+ boneAnimations.insert(AZStd::make_move_iterator(fillerAnimations.begin()), AZStd::make_move_iterator(fillerAnimations.end()));
auto animItr = boneAnimations.equal_range(currentNode->mName.C_Str());
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp
index 5b43941715..e726547c98 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.cpp
@@ -98,6 +98,20 @@ namespace AZ
}
}
+ aiMatrix4x4 CalculateWorldTransform(const aiNode* currentNode)
+ {
+ aiMatrix4x4 transform = {};
+ const aiNode* iteratingNode = currentNode;
+
+ while (iteratingNode)
+ {
+ transform = iteratingNode->mTransformation * transform;
+ iteratingNode = iteratingNode->mParent;
+ }
+
+ return transform;
+ }
+
Events::ProcessingResult AssImpBoneImporter::ImportBone(AssImpNodeEncounteredContext& context)
{
AZ_TraceContext("Importer", "Bone");
@@ -111,12 +125,7 @@ namespace AZ
}
bool isBone = false;
-
- if (NodeParentIsOfType(context.m_scene.GetGraph(), context.m_currentGraphPosition, DataTypes::IBoneData::TYPEINFO_Uuid()))
- {
- isBone = true;
- }
- else
+
{
AZStd::unordered_map mainBoneList;
AZStd::unordered_map boneLookup;
@@ -170,15 +179,8 @@ namespace AZ
{
createdBoneData = AZStd::make_shared();
}
-
- aiMatrix4x4 transform = currentNode->mTransformation;
- const aiNode* parent = currentNode->mParent;
- while (parent)
- {
- transform = parent->mTransformation * transform;
- parent = parent->mParent;
- }
+ aiMatrix4x4 transform = CalculateWorldTransform(currentNode);
SceneAPI::DataTypes::MatrixType globalTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(transform);
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.h b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.h
index bc4bdd474e..86069cdd9e 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.h
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpBoneImporter.h
@@ -31,7 +31,7 @@ namespace AZ
~AssImpBoneImporter() override = default;
static void Reflect(ReflectContext* context);
-
+
Events::ProcessingResult ImportBone(AssImpNodeEncounteredContext& context);
};
} // namespace FbxSceneBuilder
diff --git a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp
index bcc007e3a7..f7a85a161b 100644
--- a/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp
+++ b/Code/Tools/SceneAPI/FbxSceneBuilder/Importers/AssImpTransformImporter.cpp
@@ -46,8 +46,9 @@ namespace AZ
serializeContext->Class()->Version(1);
}
}
-
- void GetAllBones(const aiScene* scene, AZStd::unordered_map& boneLookup)
+
+ void GetAllBones(
+ const aiScene* scene, AZStd::unordered_multimap& boneLookup)
{
for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
{
@@ -57,7 +58,7 @@ namespace AZ
{
const aiBone* bone = mesh->mBones[boneIndex];
- boneLookup[bone->mName.C_Str()] = bone;
+ boneLookup.emplace(bone->mName.C_Str(), bone);
}
}
}
@@ -73,41 +74,53 @@ namespace AZ
return Events::ProcessingResult::Ignored;
}
- AZStd::unordered_map boneLookup;
+ AZStd::unordered_multimap boneLookup;
GetAllBones(scene, boneLookup);
auto boneIterator = boneLookup.find(currentNode->mName.C_Str());
const bool isBone = boneIterator != boneLookup.end();
-
- aiMatrix4x4 combinedTransform;
+
+ DataTypes::MatrixType localTransform;
if (isBone)
{
- auto parentNode = currentNode->mParent;
+ AZStd::vector offsets, inverseOffsets;
+ auto iteratingNode = currentNode;
- aiMatrix4x4 offsetMatrix = boneIterator->second->mOffsetMatrix;
- aiMatrix4x4 parentOffset {};
-
- auto parentBoneIterator = boneLookup.find(parentNode->mName.C_Str());
-
- if (parentNode && parentBoneIterator != boneLookup.end())
+ while (iteratingNode && boneLookup.count(iteratingNode->mName.C_Str()))
{
- const auto& parentBone = parentBoneIterator->second;
+ AZStd::string name = iteratingNode->mName.C_Str();
- parentOffset = parentBone->mOffsetMatrix;
+ auto range = boneLookup.equal_range(name);
+
+ if (range.first != range.second)
+ {
+ // There can be multiple offsetMatrices for a given bone, we're only interested in grabbing the first one
+ auto boneFirstOffsetMatrix = range.first->second->mOffsetMatrix;
+ auto azMat = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(boneFirstOffsetMatrix);
+ offsets.push_back(azMat);
+ inverseOffsets.push_back(azMat.GetInverseFull());
+ }
+
+ iteratingNode = iteratingNode->mParent;
}
-
- auto inverseOffset = offsetMatrix;
- inverseOffset.Inverse();
-
- combinedTransform = parentOffset * inverseOffset;
+
+ localTransform =
+ offsets.at(AZ::GetMin(offsets.size()-1, static_cast(1))) // parent bone offset, or if there is no parent, then current node offset
+ * inverseOffsets.at(inverseOffsets.size() - 1) // Inverse of root bone offset
+ * offsets.at(offsets.size() - 1) // Root bone offset
+ * inverseOffsets.at(0); // Inverse of current node offset
}
else
{
- combinedTransform = GetConcatenatedLocalTransform(currentNode);
+ localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(GetConcatenatedLocalTransform(currentNode));
}
- DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
+ // Don't bother adding a node with the identity matrix
+ if (localTransform == DataTypes::MatrixType::Identity())
+ {
+ return Events::ProcessingResult::Ignored;
+ }
context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
context.m_sourceSceneSystem.ConvertUnit(localTransform);
diff --git a/Code/Tools/Standalone/Source/LUA/LUAEditorMainWindow.cpp b/Code/Tools/Standalone/Source/LUA/LUAEditorMainWindow.cpp
index a08e6f4d7f..aa668921d7 100644
--- a/Code/Tools/Standalone/Source/LUA/LUAEditorMainWindow.cpp
+++ b/Code/Tools/Standalone/Source/LUA/LUAEditorMainWindow.cpp
@@ -1765,16 +1765,8 @@ namespace LUAEditor
return false;
}
- //name has the full path in it, we need to convert it to an asset name
- AZStd::string projectRoot, databaseRoot, databasePath, databaseFile, fileExtension;
- if (!AzFramework::StringFunc::AssetDatabasePath::Split(name.toUtf8().data(), &projectRoot, &databaseRoot, &databasePath, &databaseFile, &fileExtension))
- {
- AZ_Warning("LUAEditorMainWindow", false, AZStd::string::format("Path is invalid: '%s'", name.toUtf8().data()).c_str());
- return false;
- }
-
AzFramework::StringFunc::Path::Split(name.toUtf8().data(), nullptr, &m_lastOpenFilePath);
- AzFramework::StringFunc::AssetDatabasePath::Join(databasePath.c_str(), databaseFile.c_str(), newAssetName);
+ newAssetName = name.toUtf8().data();
return true;
}
diff --git a/Gems/AWSClientAuth/Code/Source/Authentication/AWSCognitoAuthenticationProvider.cpp b/Gems/AWSClientAuth/Code/Source/Authentication/AWSCognitoAuthenticationProvider.cpp
index 74865c0044..20f5bf68cc 100644
--- a/Gems/AWSClientAuth/Code/Source/Authentication/AWSCognitoAuthenticationProvider.cpp
+++ b/Gems/AWSClientAuth/Code/Source/Authentication/AWSCognitoAuthenticationProvider.cpp
@@ -146,12 +146,12 @@ namespace AWSClientAuth
void AWSCognitoAuthenticationProvider::DeviceCodeGrantSignInAsync()
{
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
void AWSCognitoAuthenticationProvider::DeviceCodeGrantConfirmSignInAsync()
{
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
void AWSCognitoAuthenticationProvider::RefreshTokensAsync()
diff --git a/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp b/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp
index d4d2d0d67b..fe8c526a11 100644
--- a/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp
+++ b/Gems/AWSClientAuth/Code/Source/Authentication/AuthenticationProviderManager.cpp
@@ -53,7 +53,7 @@ namespace AWSClientAuth
if (!m_settingsRegistry->MergeSettingsFile(resolvedPath.data(), AZ::SettingsRegistryInterface::Format::JsonMergePatch))
{
- AZ_Error("AuthenticationProviderManager", true, "Error merging settings registry for path: %s", resolvedPath.data());
+ AZ_Error("AuthenticationProviderManager", false, "Error merging settings registry for path: %s", resolvedPath.data());
return false;
}
@@ -199,7 +199,7 @@ namespace AWSClientAuth
{
return enumValue.value();
}
- AZ_Warning("AuthenticationProviderManager", true, "Incorrect string value for enum: %s", name.c_str());
+ AZ_Warning("AuthenticationProviderManager", false, "Incorrect string value for enum: %s", name.c_str());
return ProviderNameEnum::None;
}
diff --git a/Gems/AWSClientAuth/Code/Source/Authentication/GoogleAuthenticationProvider.cpp b/Gems/AWSClientAuth/Code/Source/Authentication/GoogleAuthenticationProvider.cpp
index 7762b3919b..efabaf5fc1 100644
--- a/Gems/AWSClientAuth/Code/Source/Authentication/GoogleAuthenticationProvider.cpp
+++ b/Gems/AWSClientAuth/Code/Source/Authentication/GoogleAuthenticationProvider.cpp
@@ -39,7 +39,7 @@ namespace AWSClientAuth
{
if (!settingsRegistry.lock()->GetObject(m_settings.get(), azrtti_typeid(m_settings.get()), GoogleSettingsPath))
{
- AZ_Warning("AWSCognitoAuthenticationProvider", true, "Failed to get Google settings object for path %s", GoogleSettingsPath);
+ AZ_Warning("AWSCognitoAuthenticationProvider", false, "Failed to get Google settings object for path %s", GoogleSettingsPath);
return false;
}
return true;
@@ -49,21 +49,21 @@ namespace AWSClientAuth
{
AZ_UNUSED(username);
AZ_UNUSED(password);
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
void GoogleAuthenticationProvider::PasswordGrantMultiFactorSignInAsync(const AZStd::string& username, const AZStd::string& password)
{
AZ_UNUSED(username);
AZ_UNUSED(password);
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
void GoogleAuthenticationProvider::PasswordGrantMultiFactorConfirmSignInAsync(const AZStd::string& username, const AZStd::string& confirmationCode)
{
AZ_UNUSED(username);
AZ_UNUSED(confirmationCode);
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
// Call Google authentication provider device code end point.
diff --git a/Gems/AWSClientAuth/Code/Source/Authentication/LWAAuthenticationProvider.cpp b/Gems/AWSClientAuth/Code/Source/Authentication/LWAAuthenticationProvider.cpp
index a86e01a58e..f43611f2c0 100644
--- a/Gems/AWSClientAuth/Code/Source/Authentication/LWAAuthenticationProvider.cpp
+++ b/Gems/AWSClientAuth/Code/Source/Authentication/LWAAuthenticationProvider.cpp
@@ -38,7 +38,7 @@ namespace AWSClientAuth
{
if (!settingsRegistry.lock()->GetObject(m_settings.get(), azrtti_typeid(m_settings.get()), LwaSettingsPath))
{
- AZ_Warning("AWSCognitoAuthenticationProvider", true, "Failed to get login with Amazon settings object for path %s", LwaSettingsPath);
+ AZ_Warning("AWSCognitoAuthenticationProvider", false, "Failed to get login with Amazon settings object for path %s", LwaSettingsPath);
return false;
}
return true;
@@ -48,21 +48,21 @@ namespace AWSClientAuth
{
AZ_UNUSED(username);
AZ_UNUSED(password);
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
void LWAAuthenticationProvider::PasswordGrantMultiFactorSignInAsync(const AZStd::string& username, const AZStd::string& password)
{
AZ_UNUSED(username);
AZ_UNUSED(password);
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
void LWAAuthenticationProvider::PasswordGrantMultiFactorConfirmSignInAsync(const AZStd::string& username, const AZStd::string& confirmationCode)
{
AZ_UNUSED(username);
AZ_UNUSED(confirmationCode);
- AZ_Assert(true, "Not supported");
+ AZ_Assert(false, "Not supported");
}
// Call LWA authentication provider device code end point.
diff --git a/Gems/AWSClientAuth/Code/Source/Authorization/AWSCognitoAuthorizationController.cpp b/Gems/AWSClientAuth/Code/Source/Authorization/AWSCognitoAuthorizationController.cpp
index 5e2c07bdbb..1cae2bd0b1 100644
--- a/Gems/AWSClientAuth/Code/Source/Authorization/AWSCognitoAuthorizationController.cpp
+++ b/Gems/AWSClientAuth/Code/Source/Authorization/AWSCognitoAuthorizationController.cpp
@@ -149,7 +149,7 @@ namespace AWSClientAuth
}
else
{
- AZ_Warning("AWSCognitoAuthorizationController", true, "No logins found. Fetching anonymous/unauthenticated credentials");
+ AZ_Warning("AWSCognitoAuthorizationController", false, "No logins found. Fetching anonymous/unauthenticated credentials");
}
AZ::JobContext* jobContext = nullptr;
@@ -277,7 +277,7 @@ namespace AWSClientAuth
// Check anonymous credentials as they are optional settings in Cognito Identity pool.
if (!m_cognitoCachingAnonymousCredentialsProvider->GetAWSCredentials().IsEmpty())
{
- AZ_Warning("AWSCognitoAuthorizationCredentialHandler", true, "No logins found. Using Anonymous credential provider");
+ AZ_Warning("AWSCognitoAuthorizationCredentialHandler", false, "No logins found. Using Anonymous credential provider");
return m_cognitoCachingAnonymousCredentialsProvider;
}
diff --git a/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerScriptCanvasBusTest.cpp b/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerScriptCanvasBusTest.cpp
index 7673840299..6aef72af7e 100644
--- a/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerScriptCanvasBusTest.cpp
+++ b/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerScriptCanvasBusTest.cpp
@@ -257,5 +257,5 @@ TEST_F(AuthenticationProviderManagerScriptCanvasTest, Initialize_Fail_InvalidPat
{
AZ_TEST_START_TRACE_SUPPRESSION;
ASSERT_FALSE(m_mockController->Initialize(m_enabledProviderNames, ""));
- AZ_TEST_STOP_TRACE_SUPPRESSION(1);
+ AZ_TEST_STOP_TRACE_SUPPRESSION(2);
}
diff --git a/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerTest.cpp b/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerTest.cpp
index 4b5bdfb841..ce91e29a1a 100644
--- a/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerTest.cpp
+++ b/Gems/AWSClientAuth/Code/Tests/Authentication/AuthenticationProviderManagerTest.cpp
@@ -256,5 +256,5 @@ TEST_F(AuthenticationProviderManagerTest, Initialize_Fail_InvalidPath)
{
AZ_TEST_START_TRACE_SUPPRESSION;
ASSERT_FALSE(m_mockController->Initialize(m_enabledProviderNames, ""));
- AZ_TEST_STOP_TRACE_SUPPRESSION(1);
+ AZ_TEST_STOP_TRACE_SUPPRESSION(2);
}
diff --git a/Gems/AWSClientAuth/cdk/README.md b/Gems/AWSClientAuth/cdk/README.md
index 4fe668cb9d..3bc2b59fc1 100644
--- a/Gems/AWSClientAuth/cdk/README.md
+++ b/Gems/AWSClientAuth/cdk/README.md
@@ -51,6 +51,13 @@ To add additional dependencies, for example other CDK libraries, just add
them to your requirements.txt file and rerun the `..\..\..\Lumberyard\python\pip.cmd install -r .\Gems\AWSClientAuth\cdk\requirements.txt`
command.
+
+## Update Authorization Permissions
+To give permissions to call AWS resources, please update CognitoIdentityPoolRole class with correct policy statements.
+
+An example IAM permission policy is provided to grant both authenticated and unauthenticated the permission to list S3 buckets in the project.
+However, it is expected that developers replace these permissions with those required by your users to use your resources.
+
## Useful commands
* `cdk ls` list all stacks in the app
diff --git a/Gems/AWSClientAuth/cdk/auth/cognito_identity_pool_role.py b/Gems/AWSClientAuth/cdk/auth/cognito_identity_pool_role.py
index 3a2e413617..52df897db9 100755
--- a/Gems/AWSClientAuth/cdk/auth/cognito_identity_pool_role.py
+++ b/Gems/AWSClientAuth/cdk/auth/cognito_identity_pool_role.py
@@ -53,14 +53,17 @@ class CognitoIdentityPoolRole:
}
}, assume_role_action='sts:AssumeRoleWithWebIdentity'))
- # basic permissions
+ # The above role is created for developers to add custom permissions that they need to provide authorized
+ # clients. Developers should update the policy statements below to add their required permissions.
+ # As an example s3:ListBuckets permissions are provided.
+ # Note: There must be at least one policy statement here.
stack_statement = iam.PolicyStatement(
actions=[
's3:ListBuckets'
],
effect=iam.Effect.ALLOW,
resources=[
- '*'
+ f'arn:aws:s3:::{project_name}/*'
],
sid=name_utils.format_aws_resource_sid(feature_name, project_name, iam.PolicyStatement.__name__)
)
diff --git a/Gems/AWSClientAuth/cdk/auth/cognito_user_pool_sms_role.py b/Gems/AWSClientAuth/cdk/auth/cognito_user_pool_sms_role.py
index 286b439a76..c4a442f481 100755
--- a/Gems/AWSClientAuth/cdk/auth/cognito_user_pool_sms_role.py
+++ b/Gems/AWSClientAuth/cdk/auth/cognito_user_pool_sms_role.py
@@ -32,12 +32,18 @@ class CognitoUserPoolSMSRole:
name_utils.format_aws_resource_id(feature_name, project_name, env, iam.Role.__name__),
description='Role permissions used by Cognito user pool to send sms',
assumed_by=iam.ServicePrincipal("cognito-idp.amazonaws.com"),
+ # Deny all others and then allow only for the current sms role.
inline_policies={
'SNSRoleInlinePolicy':
iam.PolicyDocument(
statements=[
+ # SMS role will be used by CognitoIDP tp allow to publish to SNS topic owned by CognitoIDP
+ # team to push a sms.
+ # Need to use * as the resource name used by CognitoIDP principal service is unknown.
iam.PolicyStatement(
- actions=["sns:Publish"], resources=["*"]
+ effect=iam.Effect.ALLOW,
+ actions=['sns:Publish'],
+ resources=['*']
)
]
)
diff --git a/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h b/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h
index c892f86b66..ab03223323 100644
--- a/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h
+++ b/Gems/AWSCore/Code/Include/Private/Editor/UI/AWSCoreEditorMenu.h
@@ -46,6 +46,7 @@ namespace AWSCore
void InitializeAWSDocActions();
void InitializeAWSGlobalDocsSubMenu();
void InitializeAWSFeatureGemActions();
+ void AddSpaceForIcon(QMenu* menu);
// AWSCoreEditorRequestBus interface implementation
void SetAWSClientAuthEnabled() override;
diff --git a/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp b/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp
index c319788547..a592c7417a 100644
--- a/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp
+++ b/Gems/AWSCore/Code/Source/Editor/UI/AWSCoreEditorMenu.cpp
@@ -35,6 +35,8 @@
namespace AWSCore
{
+ static constexpr int IconSize = 16;
+
AWSCoreEditorMenu::AWSCoreEditorMenu(const QString& text)
: QMenu(text)
, m_resourceMappingToolWatcher(nullptr)
@@ -43,6 +45,7 @@ namespace AWSCore
InitializeResourceMappingToolAction();
this->addSeparator();
InitializeAWSFeatureGemActions();
+ AddSpaceForIcon(this);
AWSCoreEditorRequestBus::Handler::BusConnect();
}
@@ -136,6 +139,8 @@ namespace AWSCore
globalDocsMenu->addAction(AddExternalLinkAction(AWSAndScriptCanvasActionText, AWSAndScriptCanvasUrl, ":/Notifications/link.svg"));
globalDocsMenu->addAction(AddExternalLinkAction(AWSAndComponentsActionText, AWSAndComponentsUrl, ":/Notifications/link.svg"));
globalDocsMenu->addAction(AddExternalLinkAction(CallAWSResourcesActionText, CallAWSResourcesUrl, ":/Notifications/link.svg"));
+
+ AddSpaceForIcon(globalDocsMenu);
}
void AWSCoreEditorMenu::InitializeAWSFeatureGemActions()
@@ -170,6 +175,8 @@ namespace AWSCore
AWSClientAuthPlatformSpecificActionText, AWSClientAuthPlatformSpecificUrl, ":/Notifications/link.svg"));
subMenu->addAction(AddExternalLinkAction(
AWSClientAuthAPIReferenceActionText, AWSClientAuthAPIReferenceUrl, ":/Notifications/link.svg"));
+
+ AddSpaceForIcon(subMenu);
}
void AWSCoreEditorMenu::SetAWSMetricsEnabled()
@@ -198,6 +205,7 @@ namespace AWSCore
QDesktopServices::openUrl(QUrl::fromLocalFile(configFilePath.c_str()));
});
subMenu->addAction(settingsAction);
+ AddSpaceForIcon(subMenu);
}
QMenu* AWSCoreEditorMenu::SetAWSFeatureSubMenu(const AZStd::string& menuText)
@@ -209,6 +217,7 @@ namespace AWSCore
{
QMenu* subMenu = new QMenu(QObject::tr(menuText.c_str()));
subMenu->setIcon(QIcon(QString(":/Notifications/checkmark.svg")));
+ subMenu->setProperty("noHover", true);
this->insertMenu(*itr, subMenu);
this->removeAction(*itr);
return subMenu;
@@ -216,4 +225,11 @@ namespace AWSCore
}
return nullptr;
}
+
+ void AWSCoreEditorMenu::AddSpaceForIcon(QMenu* menu)
+ {
+ QSize size = menu->sizeHint();
+ size.setWidth(size.width() + IconSize);
+ menu->setFixedSize(size);
+ }
} // namespace AWSCore
diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp
index 7114b50906..7697b7820b 100644
--- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp
+++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp
@@ -72,22 +72,43 @@ namespace AZ
static constexpr uint32_t ShaderVariantJobVariantParam = 3;
static constexpr uint32_t ShouldExitEarlyFromProcessJobParam = 4;
- static void AddShaderAssetJobDependency(
- AssetBuilderSDK::JobDescriptor& jobDescriptor,
- const AssetBuilderSDK::PlatformInfo& platformInfo,
- const AZStd::string& shaderVariantListFilePath,
- const AZStd::string& shaderFilePath)
+ //! Adds source file dependencies for every place a referenced file may appear, and detects if one of
+ //! those possible paths resolves to the expected file.
+ //! @param currentFilePath - the full path to the file being processed
+ //! @param referencedParentPath - the path to a reference file, which may be relative to the @currentFilePath, or may be a full asset path.
+ //! @param sourceFileDependencies - new source file dependencies will be added to this list
+ //! @param foundSourceFile - if one of the source file dependencies is found, the highest priority one will be indicated here, otherwise this will be empty.
+ //! @return true if the referenced file was found and @foundSourceFile was set
+ bool LocateReferencedSourceFile(
+ AZStd::string_view currentFilePath, AZStd::string_view referencedParentPath,
+ AZStd::vector& sourceFileDependencies,
+ AZStd::string& foundSourceFile)
{
- AZStd::vector possibleDependencies = AZ::RPI::AssetUtils::GetPossibleDepenencyPaths(shaderVariantListFilePath, shaderFilePath);
+ foundSourceFile.clear();
+
+ bool found = false;
+
+ AZStd::vector possibleDependencies = RPI::AssetUtils::GetPossibleDepenencyPaths(currentFilePath, referencedParentPath);
for (auto& file : possibleDependencies)
{
- AssetBuilderSDK::JobDependency jobDependency;
- jobDependency.m_jobKey = ShaderAssetBuilder::ShaderAssetBuilderJobKey;
- jobDependency.m_platformIdentifier = platformInfo.m_identifier;
- jobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
- jobDependency.m_sourceFile.m_sourceFileDependencyPath = file;
- jobDescriptor.m_jobDependencyList.push_back(jobDependency);
+ AssetBuilderSDK::SourceFileDependency sourceFileDependency;
+ sourceFileDependency.m_sourceFileDependencyPath = file;
+ sourceFileDependencies.push_back(sourceFileDependency);
+
+ if (!found)
+ {
+ AZ::Data::AssetInfo sourceInfo;
+ AZStd::string watchFolder;
+ AzToolsFramework::AssetSystemRequestBus::BroadcastResult(found, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetSourceInfoBySourcePath, file.c_str(), sourceInfo, watchFolder);
+
+ if (found)
+ {
+ foundSourceFile = file;
+ }
+ }
}
+
+ return found;
}
//! Returns true if @sourceFileFullPath starts with a valid asset processor scan folder, false otherwise.
@@ -334,6 +355,9 @@ namespace AZ
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
return;
}
+
+ AZStd::string foundShaderFile;
+ LocateReferencedSourceFile(variantListFullPath, shaderVariantList.m_shaderFilePath, response.m_sourceFileDependencyList, foundShaderFile);
for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
{
@@ -349,8 +373,16 @@ namespace AZ
jobDescriptor.m_jobKey = GetShaderVariantTreeAssetJobKey();
jobDescriptor.SetPlatformIdentifier(info.m_identifier.data());
-
- AddShaderAssetJobDependency(jobDescriptor, info, variantListFullPath, shaderVariantList.m_shaderFilePath);
+
+ if (!foundShaderFile.empty())
+ {
+ AssetBuilderSDK::JobDependency jobDependency;
+ jobDependency.m_jobKey = ShaderAssetBuilder::ShaderAssetBuilderJobKey;
+ jobDependency.m_platformIdentifier = info.m_identifier;
+ jobDependency.m_type = AssetBuilderSDK::JobDependencyType::Order;
+ jobDependency.m_sourceFile.m_sourceFileDependencyPath = foundShaderFile;
+ jobDescriptor.m_jobDependencyList.push_back(jobDependency);
+ }
jobDescriptor.m_jobParameters.emplace(ShaderSourceFilePathJobParam, shaderSourceFileFullPath);
diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/SrgLayoutBuilder.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/SrgLayoutBuilder.cpp
index a7721c84a2..9a3eda055e 100644
--- a/Gems/Atom/Asset/Shader/Code/Source/Editor/SrgLayoutBuilder.cpp
+++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/SrgLayoutBuilder.cpp
@@ -203,6 +203,16 @@ namespace AZ
// queue up AzslBuilder dependencies:
for (RHI::ShaderPlatformInterface* shaderPlatformInterface : platformInterfaces)
{
+ const bool isAzsli = AzFramework::StringFunc::Path::IsExtension(fullPath.c_str(), "azsli");
+ if (isAzsli)
+ {
+ auto skipCheck = ShaderBuilderUtility::ShouldSkipFileForSrgProcessing(SrgLayoutBuilderName, fullPath);
+ if (skipCheck != ShaderBuilderUtility::SrgSkipFileResult::ContinueProcess)
+ {
+ continue;
+ }
+ }
+
AddAzslBuilderJobDependency(jobDescriptor, info.m_identifier, shaderPlatformInterface->GetAPIName().GetCStr(), fullPath);
}
response.m_createJobOutputs.push_back(jobDescriptor);
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype
index ff0c4c59da..a71fc65e2a 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype
@@ -1015,7 +1015,7 @@
{
"id": "pdo",
"displayName": "Pixel Depth Offset",
- "description": "Whether to enable the pixel depth offset feature.",
+ "description": "Enable PDO to offset the original pixel depths. This will affect any shaders using depth, for example, when receiving shadows.",
"type": "Bool",
"defaultValue": false,
"connection": {
@@ -1465,14 +1465,9 @@
"file": "./EnhancedPBR_DepthPass_WithPS.shader",
"tag": "DepthPass_WithPS"
},
- // [GFX TODO][ATOM-4726] Use an "isSkinnedMesh" external material property and a functor that enables/disables the appropriate motion-vector shader
{
- "file": "Shaders/MotionVector/StaticMeshMotionVector.shader",
- "tag": "StaticMeshMotionVector"
- },
- {
- "file": "Shaders/MotionVector/SkinnedMeshMotionVector.shader",
- "tag": "SkinnedMeshMotionVector"
+ "file": "Shaders/MotionVector/MeshMotionVector.shader",
+ "tag": "MeshMotionVector"
},
// Used by the light culling system to produce accurate depth bounds for this object when it uses blended transparency
{
@@ -1669,4 +1664,3 @@
"UV1": "Unwrapped"
}
}
-
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype
index dfe2fad60f..fe86576cf8 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype
@@ -980,14 +980,9 @@
"file": "Shaders/Depth/DepthPass.shader",
"tag": "DepthPass"
},
- // [GFX TODO][ATOM-4726] Use an "isSkinnedMesh" external material property and a functor that enables/disables the appropriate motion-vector shader
{
- "file": "Shaders/MotionVector/StaticMeshMotionVector.shader",
- "tag": "StaticMeshMotionVector"
- },
- {
- "file": "Shaders/MotionVector/SkinnedMeshMotionVector.shader",
- "tag": "SkinnedMeshMotionVector"
+ "file": "Shaders/MotionVector/MeshMotionVector.shader",
+ "tag": "MeshMotionVector"
}
],
"functors": [
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype
index c07eac3d47..bccb530eb4 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype
@@ -417,7 +417,7 @@
{
"id": "pdo",
"displayName": "Pixel Depth Offset",
- "description": "Whether to enable the pixel depth offset feature.",
+ "description": "Enable PDO to offset the original pixel depths. This will affect any shaders using depth, for example, when receiving shadows.",
"type": "Bool",
"defaultValue": false,
"connection": {
@@ -2632,14 +2632,9 @@
"file": "./StandardMultilayerPBR_DepthPass_WithPS.shader",
"tag": "DepthPass_WithPS"
},
- // [GFX TODO][ATOM-4726] Use an "isSkinnedMesh" external material property and a functor that enables/disables the appropriate motion-vector shader
{
- "file": "Shaders/MotionVector/StaticMeshMotionVector.shader",
- "tag": "StaticMeshMotionVector"
- },
- {
- "file": "Shaders/MotionVector/SkinnedMeshMotionVector.shader",
- "tag": "SkinnedMeshMotionVector"
+ "file": "Shaders/MotionVector/MeshMotionVector.shader",
+ "tag": "MeshMotionVector"
}
],
"functors": [
@@ -3103,4 +3098,3 @@
"UV1": "Unwrapped"
}
}
-
diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype
index 2d94f66edf..93220973df 100644
--- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype
+++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype
@@ -956,7 +956,7 @@
{
"id": "pdo",
"displayName": "Pixel Depth Offset",
- "description": "Whether to enable the pixel depth offset feature.",
+ "description": "Enable PDO to offset the original pixel depths. This will affect any shaders using depth, for example, when receiving shadows.",
"type": "Bool",
"defaultValue": false,
"connection": {
@@ -1030,14 +1030,9 @@
"file": "./StandardPBR_DepthPass_WithPS.shader",
"tag": "DepthPass_WithPS"
},
- // [GFX TODO][ATOM-4726] Use an "isSkinnedMesh" external material property and a functor that enables/disables the appropriate motion-vector shader
{
- "file": "Shaders/MotionVector/StaticMeshMotionVector.shader",
- "tag": "StaticMeshMotionVector"
- },
- {
- "file": "Shaders/MotionVector/SkinnedMeshMotionVector.shader",
- "tag": "SkinnedMeshMotionVector"
+ "file": "Shaders/MotionVector/MeshMotionVector.shader",
+ "tag": "MeshMotionVector"
},
// Used by the light culling system to produce accurate depth bounds for this object when it uses blended transparency
{
@@ -1187,4 +1182,3 @@
"UV1": "Unwrapped"
}
}
-
diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ReflectionCopyFrameBuffer.pass b/Gems/Atom/Feature/Common/Assets/Passes/ReflectionCopyFrameBuffer.pass
index ac7ea3754c..ee83e60621 100644
--- a/Gems/Atom/Feature/Common/Assets/Passes/ReflectionCopyFrameBuffer.pass
+++ b/Gems/Atom/Feature/Common/Assets/Passes/ReflectionCopyFrameBuffer.pass
@@ -21,7 +21,7 @@
"SlotType": "Output",
"ScopeAttachmentUsage": "RenderTarget",
"LoadStoreAction": {
- "LoadAction": "Load"
+ "LoadAction": "DontCare"
}
}
],
diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli
index 893df85e3e..03d00d21f9 100644
--- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli
+++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli
@@ -427,6 +427,7 @@ float DirectionalLightShadow::SamplePcfBicubic()
shadowCoord.y >= 0. && shadowCoord.y * size < size - PixelMargin &&
shadowCoord.z < 1. - DepthMargin)
{
+ m_debugInfo.m_cascadeIndex = indexOfCascade;
return SamplePcfBicubic(shadowCoord, indexOfCascade);
}
}
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.azsl
new file mode 100644
index 0000000000..3fb3fc0fd8
--- /dev/null
+++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.azsl
@@ -0,0 +1,86 @@
+/*
+* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
+* its licensors.
+*
+* For complete copyright and license terms please see the LICENSE at the root of this
+* distribution (the "License"). All use of this software is governed by the License,
+* or, if provided, by the license below or the license accompanying this file. Do not
+* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+*
+*/
+
+#include
+#include
+
+#include
+#include
+
+struct VSInput
+{
+ float3 m_position : POSITION;
+
+ // This gets set automatically by the system at runtime only if it's available.
+ // There is a soft naming convention that associates this with o_prevPosition_isBound, which will be set to true whenever m_optional_prevPosition is available.
+ // (search "m_optional_" in ShaderVariantAssetBuilder for details on the naming convention).
+ // [GFX TODO][ATOM-14475]: Come up with a more elegant way to associate the isBound flag with the input stream.
+ // Vertex position of last frame to capture small scale motion due to vertex animation
+ float3 m_optional_prevPosition : POSITIONT;
+};
+
+struct VSOutput
+{
+ float4 m_position : SV_Position;
+ float3 m_worldPos : TEXCOORD0;
+ float3 m_worldPosPrev: TEXCOORD1;
+};
+
+struct PSOutput
+{
+ float2 m_motion : SV_Target0;
+};
+
+// Indicates whether the vertex input struct's "m_optional_prevPosition" is bound. If false, it is not safe to read from m_optional_prevPosition.
+// This option gets set automatically by the system at runtime; there is a soft naming convention that associates it with m_optional_prevPosition.
+// (search "m_optional_" in ShaderVariantAssetBuilder for details on the naming convention).
+// [GFX TODO][ATOM-14475]: Come up with a more elegant way to associate the isBound flag with the input stream.
+option bool o_prevPosition_isBound;
+
+VSOutput MainVS(VSInput IN)
+{
+ VSOutput OUT;
+
+ OUT.m_worldPos = mul(SceneSrg::GetObjectToWorldMatrix(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz;
+ OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(OUT.m_worldPos, 1.0));
+
+ if (o_prevPosition_isBound)
+ {
+ OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_optional_prevPosition, 1.0)).xyz;
+ }
+ else
+ {
+ OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz;
+ }
+
+ return OUT;
+}
+
+PSOutput MainPS(VSOutput IN)
+{
+ PSOutput OUT;
+
+ // Current clip position
+ float4 clipPos = mul(ViewSrg::m_viewProjectionMatrix, float4(IN.m_worldPos, 1.0));
+
+ // Reprojected last frame's clip position, for skinned mesh it also implies last key frame
+ float4 clipPosPrev = mul(ViewSrg::m_viewProjectionPrevMatrix, float4(IN.m_worldPosPrev, 1.0));
+
+ float2 motion = (clipPos.xy / clipPos.w - clipPosPrev.xy / clipPosPrev.w) * 0.5;
+
+ OUT.m_motion = motion;
+
+ // Flip y to line up with uv coordinates
+ OUT.m_motion.y = -OUT.m_motion.y;
+
+ return OUT;
+}
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/StaticMeshMotionVector.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.shader
similarity index 89%
rename from Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/StaticMeshMotionVector.shader
rename to Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.shader
index 0d580b1b10..c585060f3d 100644
--- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/StaticMeshMotionVector.shader
+++ b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVector.shader
@@ -1,5 +1,5 @@
{
- "Source" : "StaticMeshMotionVector",
+ "Source" : "MeshMotionVector",
"DepthStencilState" : {
"Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" }
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorCommon.azsli b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorCommon.azsli
deleted file mode 100644
index ff2758af87..0000000000
--- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/MeshMotionVectorCommon.azsli
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
-* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
-* its licensors.
-*
-* For complete copyright and license terms please see the LICENSE at the root of this
-* distribution (the "License"). All use of this software is governed by the License,
-* or, if provided, by the license below or the license accompanying this file. Do not
-* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-*
-*/
-
-#include
-#include
-
-#include
-#include
-
-struct VSOutput
-{
- float4 m_position : SV_Position;
- float3 m_worldPos : TEXCOORD0;
- float3 m_worldPosPrev: TEXCOORD1;
-};
-
-struct PSOutput
-{
- float2 m_motion : SV_Target0;
-};
-
-PSOutput MainPS(VSOutput IN)
-{
- PSOutput OUT;
-
- // Current clip position
- float4 clipPos = mul(ViewSrg::m_viewProjectionMatrix, float4(IN.m_worldPos, 1.0));
-
- // Reprojected last frame's clip position, for skinned mesh it also implies last key frame
- float4 clipPosPrev = mul(ViewSrg::m_viewProjectionPrevMatrix, float4(IN.m_worldPosPrev, 1.0));
-
- float2 motion = (clipPos.xy / clipPos.w - clipPosPrev.xy / clipPosPrev.w) * 0.5;
-
- OUT.m_motion = motion;
-
- // Flip y to line up with uv coordinates
- OUT.m_motion.y = -OUT.m_motion.y;
-
- return OUT;
-}
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/SkinnedMeshMotionVector.azsl b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/SkinnedMeshMotionVector.azsl
deleted file mode 100644
index dcbba22b2a..0000000000
--- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/SkinnedMeshMotionVector.azsl
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
-* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
-* its licensors.
-*
-* For complete copyright and license terms please see the LICENSE at the root of this
-* distribution (the "License"). All use of this software is governed by the License,
-* or, if provided, by the license below or the license accompanying this file. Do not
-* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
-* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-*
-*/
-
-#include "./MeshMotionVectorCommon.azsli"
-
-struct VSInput
-{
- float3 m_position : POSITION;
- // Vertex position of last frame to capture small scale motion due to vertex animation
- float3 m_prevPosition : POSITIONT;
-};
-
-VSOutput MainVS(VSInput IN)
-{
- VSOutput OUT;
-
- OUT.m_worldPos = mul(SceneSrg::GetObjectToWorldMatrix(ObjectSrg::m_objectId), float4(IN.m_position, 1.0)).xyz;
- OUT.m_position = mul(ViewSrg::m_viewProjectionMatrix, float4(OUT.m_worldPos, 1.0));
- OUT.m_worldPosPrev = mul(SceneSrg::GetObjectToWorldMatrixPrev(ObjectSrg::m_objectId), float4(IN.m_prevPosition, 1.0)).xyz;
-
- return OUT;
-}
-
-
-
diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/SkinnedMeshMotionVector.shader b/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/SkinnedMeshMotionVector.shader
deleted file mode 100644
index 66d2fd88c5..0000000000
--- a/Gems/Atom/Feature/Common/Assets/Shaders/MotionVector/SkinnedMeshMotionVector.shader
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "Source" : "SkinnedMeshMotionVector",
-
- "DepthStencilState" : {
- "Depth" : { "Enable" : true, "CompareFunc" : "GreaterEqual" }
- },
-
- "DrawList" : "motion",
-
- "ProgramSettings":
- {
- "EntryPoints":
- [
- {
- "name": "MainVS",
- "type": "Vertex"
- },
- {
- "name": "MainPS",
- "type": "Fragment"
- }
- ]
- }
-}
diff --git a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake
index 3dfabc586a..6a2721d4af 100644
--- a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake
+++ b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake
@@ -354,11 +354,8 @@ set(FILES
Shaders/MorphTargets/MorphTargetSRG.azsli
Shaders/MotionVector/CameraMotionVector.azsl
Shaders/MotionVector/CameraMotionVector.shader
- Shaders/MotionVector/MeshMotionVectorCommon.azsli
- Shaders/MotionVector/SkinnedMeshMotionVector.azsl
- Shaders/MotionVector/SkinnedMeshMotionVector.shader
- Shaders/MotionVector/StaticMeshMotionVector.azsl
- Shaders/MotionVector/StaticMeshMotionVector.shader
+ Shaders/MotionVector/MeshMotionVector.azsl
+ Shaders/MotionVector/MeshMotionVector.shader
Shaders/PostProcessing/AcesOutputTransformLut.azsl
Shaders/PostProcessing/AcesOutputTransformLut.shader
Shaders/PostProcessing/ApplyShaperLookupTable.azsl
diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
index 0d61ef82d1..391a60a68d 100644
--- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
+++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Mesh/MeshFeatureProcessor.h
@@ -72,7 +72,6 @@ namespace AZ
void UpdateDrawPackets(bool forceUpdate = false);
void BuildCullable();
void UpdateCullBounds(const TransformServiceFeatureProcessor* transformService);
- void SelectMotionVectorShader(Data::Instance material);
void UpdateObjectSrg();
bool MaterialRequiresForwardPassIblSpecular(Data::Instance material) const;
@@ -82,11 +81,11 @@ namespace AZ
RPI::Cullable m_cullable;
MaterialAssignmentMap m_materialAssignments;
+ MeshHandleDescriptor m_descriptor;
Data::Instance