Merge branch 'stabilization/2106' into Helios_AssImpAnimationOnAllBones

This commit is contained in:
amzn-mike
2021-06-23 09:59:43 -05:00
412 changed files with 2769 additions and 2187 deletions
@@ -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.
@@ -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;
@@ -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<AZ::IO::StorageDriveWin>(AZStd::vector<AZStd::string_view>{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 <benchmark/benchmark.h>
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<char[]> buffer(new char[FileSize]);
::memset(buffer.get(), 'c', FileSize);
file.Write(buffer.get(), FileSize);
file.Close();
AZStd::optional<AZ::IO::FixedMaxPathString> 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<StreamStackEntry> storageDriveWin =
AZStd::make_shared<StorageDriveWin>(AZStd::vector<AZStd::string_view>{ drive }, 32, 32, 4_kib, 512, 8, 0, options);
AZStd::unique_ptr<Scheduler> stack = AZStd::make_unique<Scheduler>(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<char[]> buffer(new char[FileSize]);
for (auto _ : state)
{
AZStd::binary_semaphore waitForReads;
AZStd::atomic<system_clock::time_point> 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<duration<double>>(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
@@ -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
@@ -250,10 +250,47 @@ namespace AzFramework
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
{
return AZ::IdUtils::Remapper<AZ::EntityId, true>::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<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext);
}
void SpawnableEntitiesManager::InitializeEntityIdMappings(
const Spawnable::EntityList& entities, EntityIdMap& idMap, AZStd::unordered_set<AZ::EntityId>& 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<AZ::EntityId>& 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);
}
@@ -85,6 +85,22 @@ namespace AzFramework
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
static constexpr uint32_t Processing = AZStd::numeric_limits<uint32_t>::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<AZ::EntityId> m_previouslySpawned;
AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> 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<AZ::EntityId>& previouslySpawned);
void RefreshEntityIdMapping(
const AZ::EntityId& entityId, EntityIdMap& idMap, AZStd::unordered_set<AZ::EntityId>& previouslySpawned);
Queue m_highPriorityQueue;
Queue m_regularPriorityQueue;
@@ -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<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<ComponentWithEntityReference, AZ::Component>()
->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; i<numElements; ++i)
{
@@ -123,6 +153,94 @@ namespace UnitTest
}
}
enum class EntityReferenceScheme
{
AllReferenceFirst,
AllReferenceLast,
AllReferenceThemselves,
AllReferenceNextCircular,
AllReferencePreviousCircular
};
void CreateEntityReferences(EntityReferenceScheme refScheme)
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
size_t numElements = entities.size();
for (size_t i = 0; i < numElements; ++i)
{
AZStd::unique_ptr<AZ::Entity>& entity = entities[i];
auto component = entity->CreateComponent<ComponentWithEntityReference>();
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<ComponentWithEntityReference>();
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<AzFramework::Spawnable>* 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<ComponentWithEntityReference>();
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<ComponentWithEntityReference>();
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)
{
{
@@ -31,5 +31,6 @@
<file>CarrotArrowDown.svg</file>
<file>Summary.svg</file>
<file>WindowClose.svg</file>
<file>Warning.svg</file>
</qresource>
</RCC>
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="#444444"/>
<rect x="10" y="6" width="4" height="15" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2L22 22H2L12 2ZM13 20V18H11V20H13ZM13 7H11V16.0862H13V7Z" fill="#F0C32D"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

@@ -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;
@@ -16,7 +16,7 @@
#include <QLineEdit>
#include <QMessageBox>
#include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h>
#include <FormFolderBrowseEditWidget.h>
#include <PythonBindingsInterface.h>
#include <PathValidator.h>
@@ -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"));
@@ -11,13 +11,9 @@
*/
#include <FormBrowseEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <QPushButton>
#include <QHBoxLayout>
#include <QFileDialog>
#include <QLineEdit>
#include <QStandardPaths>
#include <QIcon>
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
@@ -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
@@ -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 <FormFolderBrowseEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <QFileDialog>
#include <QLineEdit>
#include <QStandardPaths>
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
@@ -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 <FormBrowseEditWidget.h>
#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
@@ -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 <FormImageBrowseEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <QFileDialog>
#include <QLineEdit>
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
@@ -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 <FormBrowseEditWidget.h>
#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
@@ -14,6 +14,7 @@
#include <PythonBindingsInterface.h>
#include <GemCatalog/GemListHeaderWidget.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <GemCatalog/GemRequirementDialog.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
@@ -144,12 +145,23 @@ namespace O3DE::ProjectManager
}
}
void GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
bool GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
{
IPythonBindings* pythonBindings = PythonBindingsInterface::Get();
QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
QVector<QModelIndex> 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()
@@ -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);
@@ -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";
@@ -16,6 +16,7 @@
#include <QLabel>
#include <QSpacerItem>
#include <QVBoxLayout>
#include <QIcon>
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);
@@ -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;
@@ -11,7 +11,7 @@
*/
#include <GemCatalog/GemItemDelegate.h>
#include "GemModel.h"
#include <GemCatalog/GemModel.h>
#include <QEvent>
#include <QPainter>
#include <QMouseEvent>
@@ -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;
@@ -10,11 +10,9 @@
*
*/
#include "GemListView.h"
#include "GemItemDelegate.h"
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemItemDelegate.h>
#include <QStandardItemModel>
#include <QDateTime>
#include <QPalette>
namespace O3DE::ProjectManager
{
@@ -13,7 +13,6 @@
#pragma once
#if !defined(Q_MOC_RUN)
#include "GemInfo.h"
#include <QAbstractItemModel>
#include <QItemSelectionModel>
#include <QListView>
@@ -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<QModelIndex> GemModel::GatherGemsToBeAdded() const
{
QVector<QModelIndex> result;
@@ -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<QModelIndex> GatherGemsToBeAdded() const;
QVector<QModelIndex> GatherGemsToBeRemoved() const;
@@ -84,7 +88,8 @@ namespace O3DE::ProjectManager
RoleBinarySize,
RoleFeatures,
RoleTypes,
RolePath
RolePath,
RoleRequirement
};
QHash<QString, QModelIndex> m_nameToIndexMap;
@@ -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 <GemCatalog/GemRequirementDelegate.h>
#include <GemCatalog/GemModel.h>
#include <QPainter>
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
@@ -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 <GemCatalog/GemItemDelegate.h>
#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
@@ -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 <GemCatalog/GemRequirementDialog.h>
#include <GemCatalog/GemRequirementListView.h>
#include <GemCatalog/GemRequirementFilterProxyModel.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QDialogButtonBox>
#include <QSpacerItem>
namespace O3DE::ProjectManager
{
GemRequirementDialog::GemRequirementDialog(GemModel* model, const QVector<QModelIndex>& 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
@@ -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 <QDialog>
#include <QDialogButtonBox>
#endif
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(GemModel)
class GemRequirementDialog
: public QDialog
{
Q_OBJECT // AUTOMOC
public:
explicit GemRequirementDialog(GemModel* model, const QVector<QModelIndex>& gemsToAdd, QWidget *parent = nullptr);
~GemRequirementDialog() = default;
QDialogButtonBox::ButtonRole GetButtonResult();
private:
void CancelButtonPressed();
void ContinueButtonPressed();
QDialogButtonBox::ButtonRole m_buttonResult = QDialogButtonBox::RejectRole;
};
} // namespace O3DE::ProjectManager
@@ -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 <GemCatalog/GemRequirementFilterProxyModel.h>
#include <GemCatalog/GemModel.h>
#include <QItemSelectionModel>
namespace O3DE::ProjectManager
{
GemRequirementFilterProxyModel::GemRequirementFilterProxyModel(GemModel* sourceModel, const QVector<QModelIndex>& 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
@@ -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 <AzQtComponents/Utilities/SelectionProxyModel.h>
#include <QtCore/QSortFilterProxyModel>
#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<QModelIndex>& 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<QModelIndex> m_addedGems;
};
} // namespace O3DE::ProjectManager
@@ -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 <GemCatalog/GemRequirementListView.h>
#include <GemCatalog/GemRequirementDelegate.h>
#include <QStandardItemModel>
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
@@ -0,0 +1,32 @@
/*
* 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 <QAbstractItemModel>
#include <QItemSelectionModel>
#include <QListView>
#endif
namespace O3DE::ProjectManager
{
class GemRequirementListView
: public QListView
{
Q_OBJECT // AUTOMOC
public:
explicit GemRequirementListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent = nullptr);
~GemRequirementListView() = default;
};
} // namespace O3DE::ProjectManager
@@ -12,8 +12,8 @@
#include <NewProjectSettingsScreen.h>
#include <PythonBindingsInterface.h>
#include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h>
#include <FormLineEditWidget.h>
#include <TemplateButtonWidget.h>
#include <PathValidator.h>
#include <EngineInfo.h>
@@ -11,6 +11,7 @@
*/
#include <ProjectBuilder.h>
#include <ProjectManagerDefs.h>
#include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h>
@@ -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)
@@ -11,6 +11,7 @@
*/
#include <ProjectButtonWidget.h>
#include <ProjectManagerDefs.h>
#include <AzQtComponents/Utilities/DesktopUtilities.h>
#include <QVBoxLayout>
@@ -22,12 +23,11 @@
#include <QMenu>
#include <QSpacerItem>
#include <QProgressBar>
#include <QDir>
#include <QFileInfo>
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);
}
@@ -10,33 +10,74 @@
*
*/
#include "ProjectInfo.h"
#include <ProjectInfo.h>
#include <ProjectManagerDefs.h>
#include <QDir>
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
+10 -8
View File
@@ -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
@@ -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 <QString>
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
@@ -11,7 +11,7 @@
*/
#include <ProjectSettingsScreen.h>
#include <FormBrowseEditWidget.h>
#include <FormFolderBrowseEditWidget.h>
#include <FormLineEditWidget.h>
#include <PathValidator.h>
#include <PythonBindingsInterface.h>
@@ -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);
@@ -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();
@@ -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;
}
@@ -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();
@@ -12,6 +12,7 @@
#include <ProjectsScreen.h>
#include <ProjectManagerDefs.h>
#include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h>
#include <ProjectUtils.h>
@@ -35,7 +36,6 @@
#include <QSpacerItem>
#include <QListWidget>
#include <QListWidgetItem>
#include <QFileInfo>
#include <QScrollArea>
#include <QStackedWidget>
#include <QFrame>
@@ -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)
@@ -80,8 +80,6 @@ namespace O3DE::ProjectManager
QQueue<ProjectInfo> 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;
};
@@ -12,6 +12,7 @@
#include <PythonBindings.h>
#include <ProjectManagerDefs.h>
// 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
@@ -11,6 +11,7 @@
*/
#include <GemCatalog/GemCatalogScreen.h>
#include <ProjectManagerDefs.h>
#include <PythonBindingsInterface.h>
#include <ScreenHeaderWidget.h>
#include <ScreensCtrl.h>
@@ -24,6 +25,7 @@
#include <QStackedWidget>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QDir>
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
@@ -46,6 +46,7 @@ namespace O3DE::ProjectManager
private:
void Update();
void UpdateSettingsScreen();
bool UpdateProjectSettings(bool shouldConfirm = false);
enum ScreenOrder
{
@@ -11,17 +11,43 @@
*/
#include <UpdateProjectSettingsScreen.h>
#include <FormBrowseEditWidget.h>
#include <ProjectManagerDefs.h>
#include <FormImageBrowseEditWidget.h>
#include <FormLineEditWidget.h>
#include <QVBoxLayout>
#include <QLineEdit>
#include <QDir>
#include <QLabel>
#include <QFileInfo>
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
@@ -15,6 +15,8 @@
#include <ProjectSettingsScreen.h>
#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
@@ -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
)
@@ -14,4 +14,5 @@ set(FILES
Resources/ProjectManager.qss
tests/ApplicationTests.cpp
tests/main.cpp
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 <AzCore/UnitTest/TestTypes.h>
#include <Application.h>
#include <ProjectUtils.h>
#include <ProjectManager_Test_Traits_Platform.h>
#include <QFile>
#include <QTextStream>
#include <QTimer>
#include <QApplication>
#include <QKeyEvent>
#include <QDir>
namespace O3DE::ProjectManager
{
namespace ProjectUtils
{
class ProjectManagerUtilsTests
: public ::UnitTest::ScopedAllocatorSetupFixture
{
public:
ProjectManagerUtilsTests()
{
m_application = AZStd::make_unique<ProjectManager::Application>();
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<ProjectManager::Application> 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