Merge branch 'development' into Prism/RefreshGemRepos

This commit is contained in:
nggieber
2021-10-22 07:44:37 -07:00
442 changed files with 9901 additions and 5876 deletions
@@ -89,7 +89,7 @@ class TestAllComponentsIndepthTests(object):
level_creation_expected_lines = [
"Viewport is set to the expected size: True",
"Basic level created"
"Exited game mode"
]
unexpected_lines = [
"Trace::Assert",
@@ -189,8 +189,8 @@ class TestPerformanceBenchmarkSuite(object):
"Benchmark metadata captured.",
"Pass timestamps captured.",
"CPU frame time captured.",
"Capturing complete.",
"Captured data successfully."
"Captured data successfully.",
"Exited game mode"
]
unexpected_lines = [
@@ -14,10 +14,6 @@ import editor_python_test_tools.hydra_test_utils as hydra
logger = logging.getLogger(__name__)
TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests")
@pytest.mark.parametrize("project", ["AutomatedTesting"])
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("level", ["auto_test"])
class TestAtomEditorComponentsSandbox(object):
# It requires at least one test
@@ -90,7 +90,6 @@ def run():
benchmarker.capture_cpu_frame_time(i)
general.exit_game_mode()
helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=2.0)
general.log("Capturing complete.")
if __name__ == "__main__":
@@ -215,7 +215,6 @@ def run():
ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{'AtomBasicLevelSetup'}.ppm")
general.exit_game_mode()
helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=2.0)
general.log("Basic level created")
if __name__ == "__main__":
@@ -58,3 +58,6 @@ add_subdirectory(smoke)
## AWS ##
add_subdirectory(AWS)
## Integration tests for editor testing framework ##
add_subdirectory(editor_test_testing)
@@ -9,6 +9,7 @@ from __future__ import annotations
from collections import Counter
from collections import deque
from os import path
from pathlib import Path
from PySide2 import QtWidgets
@@ -20,6 +21,10 @@ from editor_python_test_tools.utils import Report
import azlmbr.entity as entity
import azlmbr.bus as bus
import azlmbr.components as components
import azlmbr.editor as editor
import azlmbr.globals
import azlmbr.math as math
import azlmbr.prefab as prefab
import editor_python_test_tools.pyside_utils as pyside_utils
@@ -57,26 +62,46 @@ class PrefabInstance:
def __hash__(self):
return hash(self.container_entity.id)
"""
See if this instance is valid to be used with other prefab operations.
:return: Whether the target instance is valid or not.
"""
def is_valid(self) -> bool:
"""
See if this instance is valid to be used with other prefab operations.
:return: Whether the target instance is valid or not.
"""
return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs
"""
Reparent this instance to target parent entity.
The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs.
:param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next.
"""
def has_editor_prefab_component(self) -> bool:
"""
Check if the instance's container entity contains EditorPrefabComponent.
:return: Whether the container entity of target instance has EditorPrefabComponent in it or not.
"""
return editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", self.container_entity.id, azlmbr.globals.property.EditorPrefabComponentTypeId)
def is_at_position(self, expected_position):
"""
Check if the instance's container entity is at expected position given.
:return: Whether the container entity of target instance is at expected position or not.
"""
actual_position = components.TransformBus(bus.Event, "GetWorldTranslation", self.container_entity.id)
is_at_position = actual_position.IsClose(expected_position)
if not is_at_position:
Report.info(f"Prefab Instance Container Entity '{self.container_entity.id.ToString()}'\'s expected position: {expected_position.ToString()}, actual position: {actual_position.ToString()}")
return is_at_position
async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId):
"""
Reparent this instance to target parent entity.
The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs.
:param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next.
"""
container_entity_id_before_reparent = self.container_entity.id
original_parent = EditorEntity(self.container_entity.get_parent_id())
original_parent_before_reparent_children_ids = set(original_parent.get_children_ids())
original_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()}
new_parent = EditorEntity(parent_entity_id)
new_parent_before_reparent_children_ids = set(new_parent.get_children_ids())
new_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()}
pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id))
pyside_utils.run_soon(lambda: wait_for_propagation())
@@ -90,23 +115,28 @@ class PrefabInstance:
except pyside_utils.EventLoopTimeoutException:
pass
original_parent_after_reparent_children_ids = set(original_parent.get_children_ids())
original_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()}
assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \
"The children count of the Prefab Instance's original parent should be decreased by 1."
assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \
"This Prefab Instance is still a child entity of its original parent entity."
new_parent_after_reparent_children_ids = set(new_parent.get_children_ids())
new_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()}
assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \
"The children count of the Prefab Instance's new parent should be increased by 1."
container_entity_id_after_reparent = set(new_parent_after_reparent_children_ids).difference(new_parent_before_reparent_children_ids).pop()
after_before_diff = set(new_parent_after_reparent_children_ids.keys()).difference(set(new_parent_before_reparent_children_ids.keys()))
container_entity_id_after_reparent = new_parent_after_reparent_children_ids[after_before_diff.pop()]
reparented_container_entity = EditorEntity(container_entity_id_after_reparent)
reparented_container_entity_parent_id = reparented_container_entity.get_parent_id()
has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString()
assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity"
current_instance_prefab = Prefab.get_prefab(self.prefab_file_name)
current_instance_prefab.instances.remove(self)
self.container_entity = reparented_container_entity
current_instance_prefab.instances.add(self)
# This is a helper class which contains some of the useful information about a prefab template.
class Prefab:
@@ -117,31 +147,32 @@ class Prefab:
self.file_path: str = get_prefab_file_path(file_path)
self.instances: set[PrefabInstance] = set()
"""
Check if a prefab is ready to be used to generate its instances.
:param file_path: A unique file path of the target prefab.
:return: Whether the target prefab is loaded or not.
"""
@classmethod
def is_prefab_loaded(cls, file_path: str) -> bool:
"""
Check if a prefab is ready to be used to generate its instances.
:param file_path: A unique file path of the target prefab.
:return: Whether the target prefab is loaded or not.
"""
return file_path in Prefab.existing_prefabs
"""
Check if a prefab exists in the directory for files of prefab tests.
:param file_name: A unique file name of the target prefab.
:return: Whether the target prefab exists or not.
"""
@classmethod
def prefab_exists(cls, file_path: str) -> bool:
"""
Check if a prefab exists in the directory for files of prefab tests.
:param file_name: A unique file name of the target prefab.
:return: Whether the target prefab exists or not.
"""
return path.exists(get_prefab_file_path(file_path))
"""
Return a prefab which can be used immediately.
:param file_name: A unique file name of the target prefab.
:return: The prefab with given file name.
"""
@classmethod
def get_prefab(cls, file_name: str) -> Prefab:
"""
Return a prefab which can be used immediately.
:param file_name: A unique file name of the target prefab.
:return: The prefab with given file name.
"""
assert file_name, "Received an empty file_name"
if Prefab.is_prefab_loaded(file_name):
return Prefab.existing_prefabs[file_name]
@@ -151,15 +182,15 @@ class Prefab:
Prefab.existing_prefabs[file_name] = Prefab(file_name)
return new_prefab
"""
Create a prefab in memory and return it. The very first instance of this prefab will also be created.
:param entities: The entities that should form the new prefab (along with their descendants).
:param file_name: A unique file name of new prefab.
:param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name.
:return: Created Prefab object and the very first PrefabInstance object owned by the prefab.
"""
@classmethod
def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance):
"""
Create a prefab in memory and return it. The very first instance of this prefab will also be created.
:param entities: The entities that should form the new prefab (along with their descendants).
:param file_name: A unique file name of new prefab.
:param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name.
:return: Created Prefab object and the very first PrefabInstance object owned by the prefab.
"""
assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists"
new_prefab = Prefab(file_name)
@@ -169,6 +200,9 @@ class Prefab:
container_entity_id = create_prefab_result.GetValue()
container_entity = EditorEntity(container_entity_id)
children_entity_ids = container_entity.get_children_ids()
assert len(children_entity_ids) == len(entities), f"Entity count of created prefab instance does *not* match the count of given entities."
if prefab_instance_name:
container_entity.set_name(prefab_instance_name)
@@ -180,12 +214,12 @@ class Prefab:
Prefab.existing_prefabs[file_name] = new_prefab
return new_prefab, new_prefab_instance
"""
Remove target prefab instances.
:param prefab_instances: Instances to be removed.
"""
@classmethod
def remove_prefabs(cls, prefab_instances: list[PrefabInstance]):
"""
Remove target prefab instances.
:param prefab_instances: Instances to be removed.
"""
entity_ids_to_remove = []
entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances]
while entity_id_queue:
@@ -212,15 +246,89 @@ class Prefab:
instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name)
instance_deleted_prefab.instances.remove(instance)
instance = PrefabInstance()
"""
Instantiate an instance of this prefab.
:param parent_entity: The entity the prefab should be a child of in the transform hierarchy.
:param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name.
:param prefab_position: The position in world space the prefab should be instantiated in.
:return: Instantiated PrefabInstance object owned by this prefab.
"""
@classmethod
def duplicate_prefabs(cls, prefab_instances: list[PrefabInstance]):
"""
Duplicate target prefab instances.
:param prefab_instances: Instances to be duplicated.
:return: PrefabInstance objects of given prefab instances' duplicates.
"""
assert prefab_instances, "Input list of prefab instances should *not* be empty."
common_parent = EditorEntity(prefab_instances[0].container_entity.get_parent_id())
common_parent_children_ids_before_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()])
container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances]
duplicate_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DuplicateEntitiesInInstance', container_entity_ids)
assert duplicate_prefab_result.IsSuccess(), f"Prefab operation 'DuplicateEntitiesInInstance' failed. Error: {duplicate_prefab_result.GetError()}"
wait_for_propagation()
duplicate_container_entity_ids = duplicate_prefab_result.GetValue()
common_parent_children_ids_after_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()])
assert set([container_entity_id.ToString() for container_entity_id in container_entity_ids]).issubset(common_parent_children_ids_after_duplicate), \
"Provided prefab instances are *not* the children of their common parent anymore after duplication."
assert common_parent_children_ids_before_duplicate.issubset(common_parent_children_ids_after_duplicate), \
"Some children of provided entities' common parent before duplication are *not* the children of the common parent anymore after duplication."
assert len(common_parent_children_ids_after_duplicate) == len(common_parent_children_ids_before_duplicate) + len(prefab_instances), \
"The children count of the given prefab instances' common parent entity is *not* increased to the expected number."
assert EditorEntity(duplicate_container_entity_ids[0]).get_parent_id().ToString() == common_parent.id.ToString(), \
"Provided prefab instances' parent should be the same as duplicates' parent."
duplicate_instances = []
for duplicate_container_entity_id in duplicate_container_entity_ids:
prefab_file_path = prefab.PrefabPublicRequestBus(bus.Broadcast, 'GetOwningInstancePrefabPath', duplicate_container_entity_id)
assert prefab_file_path, "Returned file path should *not* be empty."
prefab_file_name = Path(prefab_file_path).stem
duplicate_instance_prefab = Prefab.get_prefab(prefab_file_name)
duplicate_instance = PrefabInstance(prefab_file_path, EditorEntity(duplicate_container_entity_id))
duplicate_instance_prefab.instances.add(duplicate_instance)
duplicate_instances.append(duplicate_instance)
return duplicate_instances
@classmethod
def detach_prefab(cls, prefab_instance: PrefabInstance):
"""
Detach target prefab instance.
:param prefab_instances: Instance to be detached.
"""
parent = EditorEntity(prefab_instance.container_entity.get_parent_id())
parent_children_ids_before_detach = set([child_id.ToString() for child_id in parent.get_children_ids()])
assert prefab_instance.has_editor_prefab_component(), f"Container entity should have EditorPrefabComponent before detachment."
detach_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DetachPrefab', prefab_instance.container_entity.id)
assert detach_prefab_result.IsSuccess(), f"Prefab operation 'DetachPrefab' failed. Error: {detach_prefab_result.GetError()}"
assert not prefab_instance.has_editor_prefab_component(), f"Container entity should *not* have EditorPrefabComponent after detachment."
parent_children_ids_after_detach = set([child_id.ToString() for child_id in parent.get_children_ids()])
assert prefab_instance.container_entity.id.ToString() in parent_children_ids_after_detach, \
"Target prefab instance's container entity id should still exists after the detachment and before the propagation."
assert len(parent_children_ids_after_detach) == len(parent_children_ids_before_detach), \
"Parent entity should still keep the same amount of children entities."
wait_for_propagation()
instance_owner_prefab = Prefab.get_prefab(prefab_instance.prefab_file_name)
instance_owner_prefab.instances.remove(prefab_instance)
prefab_instance = PrefabInstance()
def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance:
"""
Instantiate an instance of this prefab.
:param parent_entity: The entity the prefab should be a child of in the transform hierarchy.
:param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name.
:param prefab_position: The position in world space the prefab should be instantiated in.
:return: Instantiated PrefabInstance object owned by this prefab.
"""
parent_entity_id = parent_entity.id if parent_entity is not None else EntityId()
instantiate_prefab_result = prefab.PrefabPublicRequestBus(
@@ -240,4 +348,6 @@ class Prefab:
assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation."
self.instances.add(new_prefab_instance)
assert new_prefab_instance.is_at_position(prefab_position), "This prefab instance is *not* at expected position."
return new_prefab_instance
@@ -9,20 +9,17 @@ import pytest
import sys
import ly_test_tools.environment.file_system as fs
from .FileManagement import FileManagement as fm
from .utils.FileManagement import FileManagement as fm
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared')
from .base import TestAutomationBase
from base import TestAutomationBase
@pytest.mark.parametrize("platform", ["win_x64_vs2017"])
@pytest.mark.parametrize("configuration", ["profile"])
@pytest.mark.parametrize("spec", ["all"])
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
@pytest.mark.parametrize("project", ["AutomatedTesting"])
class TestUtils(TestAutomationBase):
@fm.file_revert("UtilTest_Physmaterial_Editor_TestLibrary.physmaterial", r"AutomatedTesting\Levels\Physics\Physmaterial_Editor_Test")
def test_physmaterial_editor(self, request, workspace, editor):
def test_physmaterial_editor(self, request, workspace, launcher_platform, editor):
"""
Tests functionality of physmaterial editing utility
:param workspace: Fixture containing platform and project detail
@@ -35,11 +32,11 @@ class TestUtils(TestAutomationBase):
unexpected_lines = ["Assert"]
self._run_test(request, workspace, editor, physmaterial_editor_test_module, expected_lines, unexpected_lines)
def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, editor):
def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, launcher_platform, editor):
from .utils import UtilTest_Tracer_PicksErrorsAndWarnings as testcase_module
self._run_test(request, workspace, editor, testcase_module, [], [])
def test_FileManagement_FindingFiles(self, workspace):
def test_FileManagement_FindingFiles(self, workspace, launcher_platform):
"""
Tests the functionality of "searching for files" with FileManagement._find_files()
:param workspace: ly_test_tools workspace fixture
@@ -110,7 +107,7 @@ class TestUtils(TestAutomationBase):
find_me_too_path, found_me["FindMeToo.txt"]
)
def test_FileManagement_FileBackup(self, workspace):
def test_FileManagement_FileBackup(self, workspace, launcher_platform):
"""
Tests the functionality of the file back up system via the FileManagement class
:param workspace: ly_test_tools workspace fixture
@@ -167,7 +164,7 @@ class TestUtils(TestAutomationBase):
del file_map[target_file_path]
fm._save_file_map(file_map)
def test_FileManagement_FileRestoration(self, workspace):
def test_FileManagement_FileRestoration(self, workspace, launcher_platform):
"""
Tests the restore file system via the FileManagement class
:param workspace: ly_test_tools workspace fixture
@@ -261,7 +258,7 @@ class TestUtils(TestAutomationBase):
["FindMe.txt", "FindMeToo.txt"], parent_path=r"AutomatedTesting\levels\Utils\Managed_files", search_subdirs=True
)
@fm.file_override("default.physxconfiguration", "UtilTest_PhysxConfig_Override.physxconfiguration")
def test_UtilTest_Managed_Files(self, request, workspace, editor):
def test_UtilTest_Managed_Files(self, request, workspace, editor, launcher_platform):
from .utils import UtilTest_Managed_Files as test_module
expected_lines = []
@@ -47,3 +47,11 @@ class TestAutomation(TestAutomationBase):
def test_PrefabBasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform):
from .tests import PrefabBasicWorkflow_CreateAndReparentPrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False)
def test_PrefabBasicWorkflow_CreateReparentAndDetachPrefab(self, request, workspace, editor, launcher_platform):
from .tests import PrefabBasicWorkflow_CreateReparentAndDetachPrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False)
def test_PrefabBasicWorkflow_CreateAndDuplicatePrefab(self, request, workspace, editor, launcher_platform):
from .tests import PrefabBasicWorkflow_CreateAndDuplicatePrefab as test_module
self._run_prefab_test(request, workspace, editor, test_module)
@@ -16,16 +16,15 @@ def PrefabBasicWorkflow_CreateAndDeletePrefab():
prefab_test_utils.open_base_tests_level()
# Creates a new Entity at the root level
# Asserts if creation didn't succeed
# Creates a new entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Asserts if prefab creation doesn't succeeds
# Creates a prefab from the new entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Asserts if prefab deletion fails
# Deletes the prefab instance
Prefab.remove_prefabs([car])
if __name__ == "__main__":
@@ -0,0 +1,32 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
def PrefabBasicWorkflow_CreateAndDuplicatePrefab():
CAR_PREFAB_FILE_NAME = 'car_prefab'
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.prefab_utils import Prefab
import PrefabTestUtils as prefab_test_utils
prefab_test_utils.open_base_tests_level()
# Creates a new entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Creates a prefab from the new entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Duplicates the prefab instance
Prefab.duplicate_prefabs([car])
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(PrefabBasicWorkflow_CreateAndDuplicatePrefab)
@@ -22,24 +22,23 @@ def PrefabBasicWorkflow_CreateAndReparentPrefab():
prefab_test_utils.open_base_tests_level()
# Creates a new Entity at the root level
# Asserts if creation didn't succeed
# Creates a new car entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Checks for prefab creation passed or not
# Creates a prefab from the car entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Creates another new Entity at the root level
# Creates another new wheel entity at the root level
wheel_entity = EditorEntity.create_editor_entity()
wheel_prefab_entities = [wheel_entity]
# Checks for wheel prefab creation passed or not
# Creates another prefab from the wheel entity
_, wheel = Prefab.create_prefab(
wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME)
# Checks for prefab reparenting passed or not
# Reparents the wheel prefab instance to the container entity of the car prefab instance
await wheel.ui_reparent_prefab_instance(car.container_entity.id)
run_test()
@@ -17,12 +17,11 @@ def PrefabBasicWorkflow_CreatePrefab():
prefab_test_utils.open_base_tests_level()
# Creates a new Entity at the root level
# Asserts if creation didn't succeed
# Creates a new entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Checks for prefab creation passed or not
# Creates a prefab from the new entity
Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME)
if __name__ == "__main__":
@@ -0,0 +1,51 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
def PrefabBasicWorkflow_CreateReparentAndDetachPrefab():
CAR_PREFAB_FILE_NAME = 'car_prefab'
WHEEL_PREFAB_FILE_NAME = 'wheel_prefab'
import editor_python_test_tools.pyside_utils as pyside_utils
@pyside_utils.wrap_async
async def run_test():
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.prefab_utils import Prefab
import PrefabTestUtils as prefab_test_utils
prefab_test_utils.open_base_tests_level()
# Creates a new car entity at the root level
car_entity = EditorEntity.create_editor_entity()
car_prefab_entities = [car_entity]
# Creates a prefab from the car entity
_, car = Prefab.create_prefab(
car_prefab_entities, CAR_PREFAB_FILE_NAME)
# Creates another new wheel entity at the root level
wheel_entity = EditorEntity.create_editor_entity()
wheel_prefab_entities = [wheel_entity]
# Creates another prefab from the wheel entity
_, wheel = Prefab.create_prefab(
wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME)
# Reparents the wheel prefab instance to the container entity of the car prefab instance
await wheel.ui_reparent_prefab_instance(car.container_entity.id)
# Detaches the wheel prefab instance
Prefab.detach_prefab(wheel)
run_test()
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(PrefabBasicWorkflow_CreateReparentAndDetachPrefab)
@@ -19,9 +19,8 @@ def PrefabBasicWorkflow_InstantiatePrefab():
prefab_test_utils.open_base_tests_level()
# Checks for prefab instantiation passed or not
# Instantiates a new car prefab instance
test_prefab = Prefab.get_prefab(EXISTING_TEST_PREFAB_FILE_NAME)
test_instance = test_prefab.instantiate(
prefab_position=INSTANTIATED_TEST_PREFAB_POSITION)
@@ -18,20 +18,6 @@ import azlmbr.components as components
import azlmbr.entity as entity
import azlmbr.legacy.general as general
def check_entity_at_position(entity_id, expected_entity_position):
entity_at_expected_position_result = (
"entity is at expected position",
"entity is *not* at expected position")
actual_entity_position = components.TransformBus(bus.Event, "GetWorldTranslation", entity_id)
is_at_position = actual_entity_position.IsClose(expected_entity_position)
Report.result(entity_at_expected_position_result, is_at_position)
if not is_at_position:
Report.info(f"Entity '{entity_id.ToString()}'\'s expected position: {expected_entity_position.ToString()}, actual position: {actual_entity_position.ToString()}")
return is_at_position
def check_entity_children_count(entity_id, expected_children_count):
entity_children_count_matched_result = (
"Entity with a unique name found",
@@ -47,19 +33,6 @@ def check_entity_children_count(entity_id, expected_children_count):
return entity_children_count_matched
def get_children_ids_by_name(entity_id, entity_name):
entity = EditorEntity(entity_id)
children_entity_ids = entity.get_children_ids()
result = []
for child_entity_id in children_entity_ids:
child_entity = EditorEntity(child_entity_id)
child_entity_name = child_entity.get_name()
if child_entity_name == entity_name:
result.append(child_entity_id)
return result
def open_base_tests_level():
helper.init_idle()
helper.open_level("Prefab", "Base")
@@ -0,0 +1,24 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
# This timeouts on jenkins, investigation is needed. Commment for now
#
#if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS)
# ly_add_pytest(
# NAME AutomatedTesting::EditorTestTesting
# TEST_SUITE main
# TEST_SERIAL
# PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py
# RUNTIME_DEPENDENCIES
# Legacy::Editor
# AZ::AssetProcessor
# AutomatedTesting.Assets
# COMPONENT
# TestTools
# )
#endif()
@@ -35,10 +35,11 @@ class TestEditorTest:
@classmethod
def setup_class(cls):
TestEditorTest.args = sys.argv.copy()
build_dir_arg_index = TestEditorTest.args.index("--build-directory")
if build_dir_arg_index < 0:
print("Error: Must pass --build-directory argument in order to run this test")
sys.exit(-2)
build_dir_arg_index = -1
try:
build_dir_arg_index = TestEditorTest.args.index("--build-directory")
except ValueError as ex:
raise ValueError("Must pass --build-directory argument in order to run this test")
TestEditorTest.args[build_dir_arg_index+1] = os.path.abspath(TestEditorTest.args[build_dir_arg_index+1])
TestEditorTest.args.append("-s")
@@ -9,6 +9,7 @@ import os
import pytest
import ly_test_tools.environment.file_system as file_system
import ly_test_tools._internal.pytest_plugin as internal_plugin
from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite
@@ -79,6 +80,8 @@ class TestAutomation(EditorTestSuite):
class test_LandscapeCanvas_GradientNodes_EntityRemovedOnNodeDelete(EditorSharedTest):
from .EditorScripts import GradientNodes_EntityRemovedOnNodeDelete as test_module
@pytest.mark.skipif("debug" == os.path.basename(internal_plugin.build_directory),
reason="https://github.com/o3de/o3de/issues/4872")
class test_LandscapeCanvas_GraphUpdates_UpdateComponents(EditorSharedTest):
from .EditorScripts import GraphUpdates_UpdateComponents as test_module
-923
View File
@@ -1,923 +0,0 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "EditorDefs.h"
#include "ColorGradientCtrl.h"
// Qt
#include <QPainter>
#include <QToolTip>
// AzQtComponents
#include <AzQtComponents/Components/Widgets/ColorPicker.h>
#define MIN_TIME_EPSILON 0.01f
//////////////////////////////////////////////////////////////////////////
CColorGradientCtrl::CColorGradientCtrl(QWidget* parent)
: QWidget(parent)
{
m_nActiveKey = -1;
m_nHitKeyIndex = -1;
m_nKeyDrawRadius = 3;
m_bTracking = false;
m_pSpline = nullptr;
m_fMinTime = -1;
m_fMaxTime = 1;
m_fMinValue = -1;
m_fMaxValue = 1;
m_fTooltipScaleX = 1;
m_fTooltipScaleY = 1;
m_bNoTimeMarker = true;
m_bLockFirstLastKey = false;
m_bNoZoom = true;
ClearSelection();
m_bSelectedKeys.reserve(0);
m_fTimeMarker = -10;
m_grid.zoom.x = 100;
setMouseTracking(true);
}
CColorGradientCtrl::~CColorGradientCtrl()
{
}
/////////////////////////////////////////////////////////////////////////////
// QColorGradientCtrl message handlers
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
QRect rc(QPoint(0, 0), event->size());
m_rcGradient = rc;
m_rcGradient.setHeight(m_rcGradient.height() - 11);
//m_rcGradient.DeflateRect(4,4);
m_grid.rect = m_rcGradient;
if (m_bNoZoom)
{
m_grid.zoom.x = static_cast<f32>(m_grid.rect.width());
}
m_rcKeys = rc;
m_rcKeys.setTop(m_rcKeys.bottom() - 10);
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetZoom(float fZoom)
{
m_grid.zoom.x = fZoom;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetOrigin(float fOffset)
{
m_grid.origin.x = fOffset;
}
//////////////////////////////////////////////////////////////////////////
QPoint CColorGradientCtrl::KeyToPoint(int nKey)
{
if (nKey >= 0)
{
return TimeToPoint(m_pSpline->GetKeyTime(nKey));
}
return QPoint(0, 0);
}
//////////////////////////////////////////////////////////////////////////
QPoint CColorGradientCtrl::TimeToPoint(float time)
{
return QPoint(m_grid.WorldToClient(Vec2(time, 0)).x(), m_rcGradient.height() / 2);
}
//////////////////////////////////////////////////////////////////////////
AZ::Color CColorGradientCtrl::TimeToColor(float time)
{
ISplineInterpolator::ValueType val;
m_pSpline->Interpolate(time, val);
const AZ::Color col = ValueToColor(val);
return col;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val)
{
time = XOfsToTime(point.x());
ColorToValue(TimeToColor(time), val);
}
//////////////////////////////////////////////////////////////////////////
float CColorGradientCtrl::XOfsToTime(int x)
{
return m_grid.ClientToWorld(QPoint(x, 0)).x;
}
//////////////////////////////////////////////////////////////////////////
QPoint CColorGradientCtrl::XOfsToPoint(int x)
{
return TimeToPoint(XOfsToTime(x));
}
//////////////////////////////////////////////////////////////////////////
AZ::Color CColorGradientCtrl::XOfsToColor(int x)
{
return TimeToColor(XOfsToTime(x));
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::paintEvent(QPaintEvent* e)
{
QPainter painter(this);
QRect rcClient = rect();
if (m_pSpline)
{
m_bSelectedKeys.resize(m_pSpline->GetKeyCount());
}
{
if (!isEnabled())
{
painter.setBrush(palette().button());
painter.drawRect(rcClient);
return;
}
//////////////////////////////////////////////////////////////////////////
// Fill keys backgound.
//////////////////////////////////////////////////////////////////////////
QRect rcKeys = m_rcKeys.intersected(e->rect());
painter.setBrush(palette().button());
painter.drawRect(rcKeys);
//////////////////////////////////////////////////////////////////////////
//Draw Keys and Curve
if (m_pSpline)
{
DrawGradient(e, &painter);
DrawKeys(e, &painter);
}
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::DrawGradient(QPaintEvent* e, QPainter* painter)
{
//Draw Curve
// create and select a thick, white pen
painter->setPen(QPen(QColor(128, 255, 128), 1, Qt::SolidLine));
const QRect rcClip = e->rect().intersected(m_rcGradient);
const int right = rcClip.left() + rcClip.width();
for (int x = rcClip.left(); x < right; x++)
{
const AZ::Color col = XOfsToColor(x);
QPen pen(QColor(col.GetR8(), col.GetG8(), col.GetR8(), col.GetA8()), 1, Qt::SolidLine);
painter->setPen(pen);
painter->drawLine(x, m_rcGradient.top(), x, m_rcGradient.top() + m_rcGradient.height());
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::DrawKeys(QPaintEvent* e, QPainter* painter)
{
if (!m_pSpline)
{
return;
}
// create and select a white pen
painter->setPen(QPen(QColor(0, 0, 0), 1, Qt::SolidLine));
QRect rcClip = e->rect();
m_bSelectedKeys.resize(m_pSpline->GetKeyCount());
for (int i = 0; i < m_pSpline->GetKeyCount(); i++)
{
float time = m_pSpline->GetKeyTime(i);
QPoint pt = TimeToPoint(time);
if (pt.x() < rcClip.left() - 8 || pt.x() > rcClip.left() + rcClip.width() + 8)
{
continue;
}
const AZ::Color clr = TimeToColor(time);
QBrush brush(QColor(clr.GetR8(), clr.GetG8(), clr.GetB8(), clr.GetA8()));
painter->setBrush(brush);
// Find the midpoints of the top, right, left, and bottom
// of the client area. They will be the vertices of our polygon.
QPoint pts[3];
pts[0].rx() = pt.x();
pts[0].ry() = m_rcKeys.top() + 1;
pts[1].rx() = pt.x() - 5;
pts[1].ry() = m_rcKeys.top() + 8;
pts[2].rx() = pt.x() + 5;
pts[2].ry() = m_rcKeys.top() + 8;
painter->drawPolygon(pts, 3);
if (m_bSelectedKeys[i])
{
QPen pen(QColor(200, 0, 0), 1, Qt::SolidLine);
QPen oldPen = painter->pen();
painter->setPen(pen);
painter->drawPolygon(pts, 3);
painter->setPen(oldPen);
}
}
if (!m_bNoTimeMarker)
{
QPen timePen(QColor(255, 0, 255), 1, Qt::SolidLine);
painter->setPen(timePen);
QPoint pt = TimeToPoint(m_fTimeMarker);
painter->drawLine(pt.x(), m_rcGradient.top() + 1, pt.x(), m_rcGradient.bottom() - 1);
}
}
void CColorGradientCtrl::UpdateTooltip(QPoint pos)
{
if (m_nHitKeyIndex >= 0)
{
float time = m_pSpline->GetKeyTime(m_nHitKeyIndex);
ISplineInterpolator::ValueType val;
m_pSpline->GetKeyValue(m_nHitKeyIndex, val);
AZ::Color col = TimeToColor(time);
int cont_s = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_IN_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2;
int cont_d = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_OUT_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2;
QString tipText(tr("%1 : %2,%3,%4 [%5,%6]").arg(time * m_fTooltipScaleX, 0, 'f', 2).arg(col.GetR8()).arg(col.GetG8()).arg(col.GetB8()).arg(cont_s).arg(cont_d));
const QPoint globalPos = mapToGlobal(pos);
QToolTip::showText(mapToGlobal(pos), tipText, this, QRect(globalPos, QSize(1, 1)));
}
}
/////////////////////////////////////////////////////////////////////////////
//Mouse Message Handlers
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::mousePressEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
{
OnLButtonDown(event);
}
else if (event->button() == Qt::RightButton)
{
OnRButtonDown(event);
}
}
void CColorGradientCtrl::OnLButtonDown([[maybe_unused]] QMouseEvent* event)
{
if (m_bTracking)
{
return;
}
if (!m_pSpline)
{
return;
}
setFocus();
switch (m_hitCode)
{
case HIT_KEY:
StartTracking();
SetActiveKey(m_nHitKeyIndex);
break;
/*
case HIT_SPLINE:
{
// Cycle the spline slope of the nearest key.
int flags = m_pSpline->GetKeyFlags(m_nHitKeyIndex);
if (m_nHitKeyDist < 0)
// Toggle left side.
flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_IN_SHIFT;
if (m_nHitKeyDist > 0)
// Toggle right side.
flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_OUT_SHIFT;
m_pSpline->SetKeyFlags(m_nHitKeyIndex, flags);
m_pSpline->Update();
SetActiveKey(-1);
SendNotifyEvent( CLRGRDN_CHANGE );
if (m_updateCallback)
m_updateCallback(this);
break;
}
*/
case HIT_NOTHING:
SetActiveKey(-1);
break;
}
update();
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnRButtonDown([[maybe_unused]] QMouseEvent* event)
{
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::mouseDoubleClickEvent(QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
if (event->button() != Qt::LeftButton)
{
return;
}
switch (m_hitCode)
{
case HIT_SPLINE:
{
int iIndex = InsertKey(event->pos());
SetActiveKey(iIndex);
EditKey(iIndex);
update();
}
break;
case HIT_KEY:
{
EditKey(m_nHitKeyIndex);
}
break;
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::mouseMoveEvent(QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
if (!m_bTracking)
{
switch (HitTest(event->pos()))
{
case HIT_SPLINE:
{
setCursor(CMFCUtils::LoadCursor(IDC_ARRWHITE));
} break;
case HIT_KEY:
{
setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCK));
} break;
default:
break;
}
}
if (m_bTracking)
{
TrackKey(event->pos());
}
if (m_bTracking || m_nHitKeyIndex >= 0)
{
UpdateTooltip(event->pos());
}
else
{
QToolTip::hideText();
}
}
void CColorGradientCtrl::mouseReleaseEvent(QMouseEvent* event)
{
if (event->button() == Qt::LeftButton)
{
OnLButtonUp(event);
}
else if (event->button() == Qt::RightButton)
{
OnRButtonUp(event);
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnLButtonUp(QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
if (m_bTracking)
{
StopTracking(event->pos());
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnRButtonUp([[maybe_unused]] QMouseEvent* event)
{
if (!m_pSpline)
{
return;
}
}
/////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetActiveKey(int nIndex)
{
ClearSelection();
//Activate New Key
if (nIndex >= 0)
{
m_bSelectedKeys[nIndex] = true;
}
m_nActiveKey = nIndex;
update();
SendNotifyEvent(CLRGRDN_ACTIVE_KEY_CHANGE);
}
/////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetSpline(ISplineInterpolator* pSpline, bool bRedraw)
{
if (pSpline != m_pSpline)
{
//if (pSpline && pSpline->GetNumDimensions() != 3)
//return;
m_pSpline = pSpline;
m_nActiveKey = -1;
}
ClearSelection();
if (bRedraw)
{
update();
}
}
//////////////////////////////////////////////////////////////////////////
ISplineInterpolator* CColorGradientCtrl::GetSpline()
{
return m_pSpline;
}
/////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::keyPressEvent(QKeyEvent* event)
{
bool bProcessed = false;
if (m_nActiveKey != -1 && m_pSpline)
{
switch (event->key())
{
case Qt::Key_Delete:
{
RemoveKey(m_nActiveKey);
bProcessed = true;
} break;
case Qt::Key_Up:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() -= 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
case Qt::Key_Down:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() += 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
case Qt::Key_Left:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() -= 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
case Qt::Key_Right:
{
CUndo undo("Move Spline Key");
QPoint point = KeyToPoint(m_nActiveKey);
point.rx() += 1;
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
TrackKey(point);
bProcessed = true;
} break;
default:
break; //do nothing
}
update();
}
event->setAccepted(bProcessed);
}
//////////////////////////////////////////////////////////////////////////////
CColorGradientCtrl::EHitCode CColorGradientCtrl::HitTest(QPoint point)
{
if (!m_pSpline)
{
return HIT_NOTHING;
}
ISplineInterpolator::ValueType val;
float time;
PointToTimeValue(point, time, val);
QRect rc = rect();
m_nHitKeyIndex = -1;
if (rc.contains(point))
{
m_nHitKeyDist = 0xFFFF;
m_hitCode = HIT_SPLINE;
for (int i = 0; i < m_pSpline->GetKeyCount(); i++)
{
QPoint splinePt = TimeToPoint(m_pSpline->GetKeyTime(i));
if (abs(point.x() - splinePt.x()) < abs(m_nHitKeyDist))
{
m_nHitKeyIndex = i;
m_nHitKeyDist = point.x() - splinePt.x();
}
}
if (abs(m_nHitKeyDist) < 4)
{
m_hitCode = HIT_KEY;
}
}
else
{
m_hitCode = HIT_NOTHING;
}
return m_hitCode;
}
///////////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::StartTracking()
{
m_bTracking = true;
GetIEditor()->BeginUndo();
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCKCROSS));
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::TrackKey(QPoint point)
{
if (point.x() < m_rcGradient.left() || point.y() > m_rcGradient.right())
{
return;
}
int nKey = m_nHitKeyIndex;
if (nKey >= 0)
{
ISplineInterpolator::ValueType val;
float time;
PointToTimeValue(point, time, val);
// Clamp to min/max time.
if (time < m_fMinTime || time > m_fMaxTime)
{
return;
}
int i;
for (i = 0; i < m_pSpline->GetKeyCount(); i++)
{
// Switch to next key.
if ((m_pSpline->GetKeyTime(i) < time && i > nKey) ||
(m_pSpline->GetKeyTime(i) > time && i < nKey))
{
m_pSpline->SetKeyTime(nKey, time);
m_pSpline->Update();
SetActiveKey(i);
m_nHitKeyIndex = i;
return;
}
}
if (!m_bLockFirstLastKey || (nKey != 0 && nKey != m_pSpline->GetKeyCount() - 1))
{
m_pSpline->SetKeyTime(nKey, time);
m_pSpline->Update();
}
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
update();
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::StopTracking(QPoint point)
{
if (!m_bTracking)
{
return;
}
GetIEditor()->AcceptUndo("Spline Move");
if (m_nHitKeyIndex >= 0)
{
QRect rc = rect();
rc = rc.marginsAdded(QMargins(100, 100, 100, 100));
if (!rc.contains(point))
{
RemoveKey(m_nHitKeyIndex);
}
}
m_bTracking = false;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::EditKey(int nKey)
{
if (!m_pSpline)
{
return;
}
if (nKey < 0 || nKey >= m_pSpline->GetKeyCount())
{
return;
}
SetActiveKey(nKey);
ISplineInterpolator::ValueType val;
m_pSpline->GetKeyValue(nKey, val);
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
AzQtComponents::ColorPicker dlg(AzQtComponents::ColorPicker::Configuration::RGB);
dlg.setCurrentColor(ValueToColor(val));
dlg.setSelectedColor(ValueToColor(val));
connect(&dlg, &AzQtComponents::ColorPicker::currentColorChanged, this, &CColorGradientCtrl::OnKeyColorChanged);
if (dlg.exec() == QDialog::Accepted)
{
CUndo undo("Modify Gradient Color");
OnKeyColorChanged(dlg.selectedColor());
}
else
{
OnKeyColorChanged(ValueToColor(val));
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::OnKeyColorChanged(const AZ::Color& color)
{
int nKey = m_nActiveKey;
if (!m_pSpline)
{
return;
}
if (nKey < 0 || nKey >= m_pSpline->GetKeyCount())
{
return;
}
ISplineInterpolator::ValueType val;
ColorToValue(color, val);
m_pSpline->SetKeyValue(nKey, val);
update();
if (m_bLockFirstLastKey)
{
if (nKey == 0)
{
m_pSpline->SetKeyValue(m_pSpline->GetKeyCount() - 1, val);
}
else if (nKey == m_pSpline->GetKeyCount() - 1)
{
m_pSpline->SetKeyValue(0, val);
}
}
m_pSpline->Update();
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
GetIEditor()->UpdateViews(eRedrawViewports);
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::RemoveKey(int nKey)
{
if (!m_pSpline)
{
return;
}
if (m_bLockFirstLastKey)
{
if (nKey == 0 || nKey == m_pSpline->GetKeyCount() - 1)
{
return;
}
}
CUndo undo("Remove Spline Key");
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
m_nActiveKey = -1;
m_nHitKeyIndex = -1;
if (m_pSpline)
{
m_pSpline->RemoveKey(nKey);
m_pSpline->Update();
}
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
update();
}
//////////////////////////////////////////////////////////////////////////
int CColorGradientCtrl::InsertKey(QPoint point)
{
CUndo undo("Spline Insert Key");
ISplineInterpolator::ValueType val;
float time;
PointToTimeValue(point, time, val);
if (time < m_fMinTime || time > m_fMaxTime)
{
return -1;
}
int i;
for (i = 0; i < m_pSpline->GetKeyCount(); i++)
{
// Skip if any key already have time that is very close.
if (fabs(m_pSpline->GetKeyTime(i) - time) < MIN_TIME_EPSILON)
{
return i;
}
}
SendNotifyEvent(CLRGRDN_BEFORE_CHANGE);
m_pSpline->InsertKey(time, val);
m_pSpline->Interpolate(time, val);
ClearSelection();
update();
SendNotifyEvent(CLRGRDN_CHANGE);
if (m_updateCallback)
{
m_updateCallback(this);
}
for (i = 0; i < m_pSpline->GetKeyCount(); i++)
{
// Find key with added time.
if (m_pSpline->GetKeyTime(i) == time)
{
return i;
}
}
return -1;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::ClearSelection()
{
m_nActiveKey = -1;
if (m_pSpline)
{
m_bSelectedKeys.resize(m_pSpline->GetKeyCount());
}
for (int i = 0; i < (int)m_bSelectedKeys.size(); i++)
{
m_bSelectedKeys[i] = false;
}
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SetTimeMarker(float fTime)
{
if (!m_pSpline)
{
return;
}
{
QPoint pt = TimeToPoint(m_fTimeMarker);
QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized();
rc += QMargins(1, 0, 1, 0);
update(rc);
}
{
QPoint pt = TimeToPoint(fTime);
QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized();
rc += QMargins(1, 0, 1, 0);
update(rc);
}
m_fTimeMarker = fTime;
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::SendNotifyEvent(int nEvent)
{
switch (nEvent)
{
case CLRGRDN_BEFORE_CHANGE:
emit beforeChange();
break;
case CLRGRDN_CHANGE:
emit change();
break;
case CLRGRDN_ACTIVE_KEY_CHANGE:
emit activeKeyChange();
break;
}
}
//////////////////////////////////////////////////////////////////////////
AZ::Color CColorGradientCtrl::ValueToColor(ISplineInterpolator::ValueType val)
{
const AZ::Color color(val[0], val[1], val[2], 1.0);
return color.LinearToGamma();
}
//////////////////////////////////////////////////////////////////////////
void CColorGradientCtrl::ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val)
{
const AZ::Color colLin = col.GammaToLinear();
val[0] = colLin.GetR();
val[1] = colLin.GetG();
val[2] = colLin.GetB();
val[3] = 0;
}
void CColorGradientCtrl::SetNoTimeMarker(bool noTimeMarker)
{
m_bNoTimeMarker = noTimeMarker;
update();
}
#include <Controls/moc_ColorGradientCtrl.cpp>
-167
View File
@@ -1,167 +0,0 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H
#define CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H
#pragma once
#if !defined(Q_MOC_RUN)
#include <QWidget>
#include <ISplines.h>
#include "Controls/WndGridHelper.h"
#endif
namespace AZ
{
class Color;
}
// Notify event sent when spline is being modified.
#define CLRGRDN_CHANGE (0x0001)
// Notify event sent just before when spline is modified.
#define CLRGRDN_BEFORE_CHANGE (0x0002)
// Notify event sent when the active key changes
#define CLRGRDN_ACTIVE_KEY_CHANGE (0x0003)
//////////////////////////////////////////////////////////////////////////
// Spline control.
//////////////////////////////////////////////////////////////////////////
class CColorGradientCtrl
: public QWidget
{
Q_OBJECT
public:
CColorGradientCtrl(QWidget* parent = nullptr);
virtual ~CColorGradientCtrl();
//Key functions
int GetActiveKey() { return m_nActiveKey; };
void SetActiveKey(int nIndex);
int InsertKey(QPoint point);
// Turns on/off zooming and scroll support.
void SetNoZoom([[maybe_unused]] bool bNoZoom) { m_bNoZoom = false; };
void SetTimeRange(float tmin, float tmax) { m_fMinTime = tmin; m_fMaxTime = tmax; }
void SetValueRange(float tmin, float tmax) { m_fMinValue = tmin; m_fMaxValue = tmax; }
void SetTooltipValueScale(float x, float y) { m_fTooltipScaleX = x; m_fTooltipScaleY = y; };
// Lock value of first and last key to be the same.
void LockFirstAndLastKeys(bool bLock) { m_bLockFirstLastKey = bLock; }
void SetSpline(ISplineInterpolator* pSpline, bool bRedraw = false);
ISplineInterpolator* GetSpline();
void SetTimeMarker(float fTime);
// Zoom in pixels per time unit.
void SetZoom(float fZoom);
void SetOrigin(float fOffset);
typedef AZStd::function<void(CColorGradientCtrl*)> UpdateCallback;
void SetUpdateCallback(const UpdateCallback& cb) { m_updateCallback = cb; };
void SetNoTimeMarker(bool noTimeMarker);
signals:
void change();
void beforeChange();
void activeKeyChange();
protected:
enum EHitCode
{
HIT_NOTHING,
HIT_KEY,
HIT_SPLINE,
};
void paintEvent(QPaintEvent* e) override;
void resizeEvent(QResizeEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void OnLButtonDown(QMouseEvent* event);
void mouseMoveEvent(QMouseEvent* event) override;
void OnLButtonUp(QMouseEvent* event);
void OnRButtonUp(QMouseEvent* event);
void mouseDoubleClickEvent(QMouseEvent* event) override;
void OnRButtonDown(QMouseEvent* event);
void keyPressEvent(QKeyEvent* event) override;
// Drawing functions
void DrawGradient(QPaintEvent* e, QPainter* painter);
void DrawKeys(QPaintEvent* e, QPainter* painter);
void UpdateTooltip(QPoint pos);
EHitCode HitTest(QPoint point);
//Tracking support helper functions
void StartTracking();
void TrackKey(QPoint point);
void StopTracking(QPoint point);
void RemoveKey(int nKey);
void EditKey(int nKey);
QPoint KeyToPoint(int nKey);
QPoint TimeToPoint(float time);
void PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val);
float XOfsToTime(int x);
QPoint XOfsToPoint(int x);
AZ::Color XOfsToColor(int x);
AZ::Color TimeToColor(float time);
void ClearSelection();
void SendNotifyEvent(int nEvent);
AZ::Color ValueToColor(ISplineInterpolator::ValueType val);
void ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val);
private:
void OnKeyColorChanged(const AZ::Color& color);
private:
ISplineInterpolator* m_pSpline;
bool m_bNoZoom;
QRect m_rcClipRect;
QRect m_rcGradient;
QRect m_rcKeys;
QPoint m_hitPoint;
EHitCode m_hitCode;
int m_nHitKeyIndex;
int m_nHitKeyDist;
QPoint m_curvePoint;
float m_fTimeMarker;
int m_nActiveKey;
int m_nKeyDrawRadius;
bool m_bTracking;
float m_fMinTime, m_fMaxTime;
float m_fMinValue, m_fMaxValue;
float m_fTooltipScaleX, m_fTooltipScaleY;
bool m_bLockFirstLastKey;
bool m_bNoTimeMarker;
std::vector<int> m_bSelectedKeys;
UpdateCallback m_updateCallback;
CWndGridHelper m_grid;
};
#endif // CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H
@@ -27,7 +27,6 @@ void RegisterReflectedVarHandlers()
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LocalStringPropertyHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LightAnimationPropertyHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew UserPopupWidgetHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew ColorCurveHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew FloatCurveHandler());
EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew MotionPropertyWidgetHandler());
}
@@ -170,30 +170,3 @@ bool FloatCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CSpline
GUI->SetSpline(reinterpret_cast<ISplineInterpolator*>(instance.m_spline));
return false;
}
QWidget* ColorCurveHandler::CreateGUI(QWidget *pParent)
{
CColorGradientCtrl* gradientCtrl = new CColorGradientCtrl(pParent);
//connect(gradientCtrl, &CColorGradientCtrl::change, [gradientCtrl]()
//{
// EBUS_EVENT(AzToolsFramework::PropertyEditorGUIMessages::Bus, RequestWrite, gradientCtrl);
//});
gradientCtrl->SetTimeRange(0, 1);
gradientCtrl->setFixedHeight(36);
return gradientCtrl;
}
void ColorCurveHandler::ConsumeAttribute(CColorGradientCtrl*, AZ::u32, AzToolsFramework::PropertyAttributeReader*, const char*)
{}
void ColorCurveHandler::WriteGUIValuesIntoProperty([[maybe_unused]] size_t index, [[maybe_unused]] CColorGradientCtrl* GUI, [[maybe_unused]] property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node)
{}
bool ColorCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CColorGradientCtrl* GUI, const property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node)
{
GUI->SetSpline(reinterpret_cast<ISplineInterpolator*>(instance.m_spline));
return false;
}
@@ -16,7 +16,6 @@
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
#include "ReflectedVar.h"
#include "Util/VariablePropertyType.h"
#include "Controls/ColorGradientCtrl.h"
#include "Controls/SplineCtrl.h"
#include <QWidget>
#endif
@@ -82,17 +81,4 @@ public:
void OnSplineChange(CSplineCtrl*);
};
class ColorCurveHandler : public QObject, public AzToolsFramework::PropertyHandler < CReflectedVarSpline, CColorGradientCtrl>
{
public:
AZ_CLASS_ALLOCATOR(ColorCurveHandler, AZ::SystemAllocator, 0);
bool IsDefaultHandler() const override { return false; }
QWidget* CreateGUI(QWidget *pParent) override;
AZ::u32 GetHandlerName(void) const override { return AZ_CRC("ePropertyColorCurve", 0xa30da4ec); }
void ConsumeAttribute(CColorGradientCtrl* GUI, AZ::u32 attrib, AzToolsFramework::PropertyAttributeReader* attrValue, const char* debugName) override;
void WriteGUIValuesIntoProperty(size_t index, CColorGradientCtrl* GUI, property_t& instance, AzToolsFramework::InstanceDataNode* node) override;
bool ReadValuesIntoGUI(size_t index, CColorGradientCtrl* GUI, const property_t& instance, AzToolsFramework::InstanceDataNode* node) override;
};
#endif // CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H
-2
View File
@@ -330,8 +330,6 @@ set(FILES
Commands/CommandManager.h
Controls/BitmapToolTip.cpp
Controls/BitmapToolTip.h
Controls/ColorGradientCtrl.cpp
Controls/ColorGradientCtrl.h
Controls/ConsoleSCB.cpp
Controls/ConsoleSCB.h
Controls/ConsoleSCB.ui
@@ -0,0 +1,254 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
namespace AZ
{
JsonSerializationResult::ResultCode JsonImportResolver::ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element)
{
using namespace JsonSerializationResult;
for (auto& path : importPathStack)
{
if (importPath == path)
{
return settings.m_reporting(
AZStd::string::format("'%s' was already imported in this chain. This indicates a cyclic dependency.", importPath.c_str()),
ResultCode(Tasks::Import, Outcomes::Catastrophic), element);
}
}
importPathStack.push_back(importPath);
AZ::StackedString importElement(AZ::StackedString::Format::JsonPointer);
JsonImportSettings nestedImportSettings;
nestedImportSettings.m_importer = settings.m_importer;
nestedImportSettings.m_reporting = settings.m_reporting;
nestedImportSettings.m_resolveFlags = ImportTracking::Dependencies;
ResultCode result = ResolveImports(jsonDoc, allocator, importPathStack, nestedImportSettings, importElement);
importPathStack.pop_back();
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject())
{
for (auto& field : jsonDoc.GetObject())
{
if(strncmp(field.name.GetString(), JsonSerialization::ImportDirectiveIdentifier, field.name.GetStringLength()) == 0)
{
const rapidjson::Value& importDirective = field.value;
AZ::IO::FixedMaxPath importAbsPath = importPathStack.back();
importAbsPath.RemoveFilename();
AZStd::string importName;
if (importDirective.IsObject())
{
auto filenameField = importDirective.FindMember("filename");
if (filenameField != importDirective.MemberEnd())
{
importName = AZStd::string(filenameField->value.GetString(), filenameField->value.GetStringLength());
}
}
else
{
importName = AZStd::string(importDirective.GetString(), importDirective.GetStringLength());
}
importAbsPath.Append(importName);
rapidjson::Value patch;
ResultCode resolveResult = settings.m_importer->ResolveImport(&jsonDoc, patch, importDirective, importAbsPath, allocator);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
if ((settings.m_resolveFlags & ImportTracking::Imports) == ImportTracking::Imports)
{
rapidjson::Pointer path(element.Get().data(), element.Get().size());
settings.m_importer->AddImportDirective(path, importName);
}
if ((settings.m_resolveFlags & ImportTracking::Dependencies) == ImportTracking::Dependencies)
{
settings.m_importer->AddImportedFile(importAbsPath.String());
}
ResultCode result = ResolveNestedImports(jsonDoc, allocator, importPathStack, settings, importAbsPath, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
settings.m_importer->ApplyPatch(jsonDoc, patch, allocator);
}
else if (field.value.IsObject() || field.value.IsArray())
{
ScopedStackedString entryName(element, AZStd::string_view(field.name.GetString(), field.name.GetStringLength()));
ResultCode result = ResolveImports(field.value, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
}
else if(jsonDoc.IsArray())
{
int index = 0;
for (rapidjson::Value::ValueIterator elem = jsonDoc.Begin(); elem != jsonDoc.End(); ++elem, ++index)
{
if (!elem->IsObject() && !elem->IsArray())
{
continue;
}
ScopedStackedString entryName(element, index);
ResultCode result = ResolveImports(*elem, allocator, importPathStack, settings, element);
if (result.GetOutcome() == Outcomes::Catastrophic)
{
return result;
}
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode JsonImportResolver::RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (jsonDoc.IsObject() || jsonDoc.IsArray())
{
const BaseJsonImporter::ImportDirectivesList& importDirectives = settings.m_importer->GetImportDirectives();
for (auto& import : importDirectives)
{
rapidjson::Pointer importPtr = import.first;
rapidjson::Value* currentValue = importPtr.Get(jsonDoc);
rapidjson::Value importedValue(rapidjson::kObjectType);
importedValue.AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(import.second.c_str()), allocator);
ResultCode resolveResult = JsonSerialization::ResolveImports(importedValue, allocator, settings);
if (resolveResult.GetOutcome() == Outcomes::Catastrophic)
{
return resolveResult;
}
rapidjson::Value patch;
settings.m_importer->CreatePatch(patch, importedValue, *currentValue, allocator);
settings.m_importer->RestoreImport(currentValue, patch, allocator, import.second);
}
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
auto importedObject = JsonSerializationUtils::ReadJsonFile(importedFilePath.Native());
if (importedObject.IsSuccess())
{
rapidjson::Value& importedDoc = importedObject.GetValue();
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
}
else
{
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, const AZStd::string& importFilename)
{
using namespace JsonSerializationResult;
importPtr->SetObject();
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
rapidjson::Value importDirective(rapidjson::kObjectType);
importDirective.AddMember(rapidjson::StringRef("filename"), rapidjson::StringRef(importFilename.c_str()), allocator);
importDirective.AddMember(rapidjson::StringRef("patch"), patch, allocator);
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), importDirective, allocator);
}
else
{
importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(importFilename.c_str()), allocator);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator)
{
using namespace JsonSerializationResult;
if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty()))
{
return AZ::JsonSerialization::ApplyPatch(target, allocator, patch, JsonMergeApproach::JsonMergePatch);
}
return ResultCode(Tasks::Import, Outcomes::Success);
}
JsonSerializationResult::ResultCode BaseJsonImporter::CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator)
{
return JsonSerialization::CreatePatch(patch, allocator, source, target, JsonMergeApproach::JsonMergePatch);
}
void BaseJsonImporter::AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile)
{
m_importDirectives.emplace_back(jsonPtr, AZStd::move(importFile));
}
void BaseJsonImporter::AddImportedFile(AZStd::string importedFile)
{
m_importedFiles.insert(AZStd::move(importedFile));
}
const BaseJsonImporter::ImportDirectivesList& BaseJsonImporter::GetImportDirectives()
{
return m_importDirectives;
}
const BaseJsonImporter::ImportedFilesList& BaseJsonImporter::GetImportedFiles()
{
return m_importedFiles;
}
} // namespace AZ
@@ -0,0 +1,108 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include<AzCore/IO/Path/Path.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/pointer.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/std/containers/unordered_set.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/Serialization/Json/JsonSerializationResult.h>
#include <AzCore/Serialization/Json/StackedString.h>
namespace AZ
{
struct JsonImportSettings;
class BaseJsonImporter
{
public:
AZ_RTTI(BaseJsonImporter, "{7B225807-7B43-430F-8B11-C794DCF5ACA5}");
using ImportDirectivesList = AZStd::vector<AZStd::pair<rapidjson::Pointer, AZStd::string>>;
using ImportedFilesList = AZStd::unordered_set<AZStd::string>;
virtual JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode RestoreImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator,
const AZStd::string& importFilename);
virtual JsonSerializationResult::ResultCode ApplyPatch(rapidjson::Value& target,
const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator);
virtual JsonSerializationResult::ResultCode CreatePatch(rapidjson::Value& patch,
const rapidjson::Value& source, const rapidjson::Value& target,
rapidjson::Document::AllocatorType& allocator);
void AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile);
const ImportDirectivesList& GetImportDirectives();
void AddImportedFile(AZStd::string importedFile);
const ImportedFilesList& GetImportedFiles();
virtual ~BaseJsonImporter() = default;
protected:
ImportDirectivesList m_importDirectives;
ImportedFilesList m_importedFiles;
};
enum class ImportTracking : AZ::u8
{
None = 0,
Dependencies = (1<<0),
Imports = (1<<1),
All = (Dependencies | Imports)
};
AZ_DEFINE_ENUM_BITWISE_OPERATORS(ImportTracking);
class JsonImportResolver final
{
public:
using ImportPathStack = AZStd::vector<AZ::IO::FixedMaxPath>;
JsonImportResolver() = delete;
JsonImportResolver& operator=(const JsonImportResolver& rhs) = delete;
JsonImportResolver& operator=(JsonImportResolver&& rhs) = delete;
JsonImportResolver(const JsonImportResolver& rhs) = delete;
JsonImportResolver(JsonImportResolver&& rhs) = delete;
~JsonImportResolver() = delete;
static JsonSerializationResult::ResultCode ResolveImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, StackedString& element);
static JsonSerializationResult::ResultCode RestoreImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
static JsonSerializationResult::ResultCode ResolveNestedImports(rapidjson::Value& jsonDoc,
rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack,
JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element);
};
struct JsonImportSettings final
{
JsonSerializationResult::JsonIssueCallback m_reporting;
BaseJsonImporter* m_importer = nullptr;
ImportTracking m_resolveFlags = ImportTracking::All;
AZ::IO::FixedMaxPath m_loadedJsonPath;
};
} // namespace AZ
@@ -706,7 +706,7 @@ namespace AZ
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
}
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -717,6 +717,10 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
// Do an extra pass to find all the fields that are removed.
@@ -751,7 +755,7 @@ namespace AZ
rapidjson::Value value;
ResultCode result = CreateMergePatchInternal(value, allocator,
rapidjson::Value(rapidjson::kNullType), field.value, element, settings);
if (result.GetOutcome() == Outcomes::Success)
if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults)
{
rapidjson::Value name;
name.CopyFrom(field.name, allocator, true);
@@ -762,11 +766,20 @@ namespace AZ
{
return result;
}
else
{
resultCode.Combine(result);
}
}
if (target.MemberCount() == 0)
{
resultCode.Combine(settings.m_reporting("Added empty object to JSON Merge Patch.",
ResultCode(Tasks::CreatePatch, Outcomes::Success), element));
}
}
patch = AZStd::move(resultValue);
resultCode.Combine(ResultCode(Tasks::CreatePatch, Outcomes::Success));
return resultCode;
}
else
@@ -10,6 +10,7 @@
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/Json/BaseJsonSerializer.h>
#include <AzCore/Serialization/Json/JsonDeserializer.h>
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonMerger.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/JsonSerializer.h>
@@ -19,11 +20,6 @@
namespace AZ
{
const char* JsonSerialization::TypeIdFieldIdentifier = "$type";
const char* JsonSerialization::DefaultStringIdentifier = "{}";
const char* JsonSerialization::KeyFieldIdentifier = "Key";
const char* JsonSerialization::ValueFieldIdentifier = "Value";
namespace JsonSerializationInternal
{
template<typename T>
@@ -394,6 +390,60 @@ namespace AZ
}
}
JsonSerializationResult::ResultCode JsonSerialization::ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
JsonImportResolver::ImportPathStack importPathStack;
importPathStack.push_back(settings.m_loadedJsonPath);
StackedString element(StackedString::Format::JsonPointer);
return JsonImportResolver::ResolveImports(jsonDoc, allocator, importPathStack, settings, element);
}
JsonSerializationResult::ResultCode JsonSerialization::RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings)
{
using namespace JsonSerializationResult;
if (settings.m_importer == nullptr)
{
AZ_Assert(false, "Importer object needs to be provided");
return ResultCode(Tasks::Import, Outcomes::Catastrophic);
}
AZStd::string scratchBuffer;
auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode
{
return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target);
};
if (!settings.m_reporting)
{
settings.m_reporting = issueReportingCallback;
}
settings.m_resolveFlags = ImportTracking::None;
return JsonImportResolver::RestoreImports(jsonDoc, allocator, settings);
}
JsonSerializationResult::ResultCode JsonSerialization::DefaultIssueReporter(AZStd::string& scratchBuffer,
AZStd::string_view message, JsonSerializationResult::ResultCode result, AZStd::string_view path)
{
@@ -18,6 +18,8 @@
namespace AZ
{
class BaseJsonSerializer;
struct JsonImportSettings;
enum class JsonMergeApproach
{
@@ -51,10 +53,11 @@ namespace AZ
class JsonSerialization final
{
public:
static const char* TypeIdFieldIdentifier;
static const char* DefaultStringIdentifier;
static const char* KeyFieldIdentifier;
static const char* ValueFieldIdentifier;
static constexpr const char* TypeIdFieldIdentifier = "$type";
static constexpr const char* DefaultStringIdentifier = "{}";
static constexpr const char* KeyFieldIdentifier = "Key";
static constexpr const char* ValueFieldIdentifier = "Value";
static constexpr const char* ImportDirectiveIdentifier = "$import";
//! Merges two json values together by applying "patch" to "target" using the selected merge algorithm.
//! This version of ApplyPatch is destructive to "target". If the patch can't be correctly applied it will
@@ -284,6 +287,22 @@ namespace AZ
//! @return An enum containing less, equal or greater. In case of an error, the value for the enum will "error".
static JsonSerializerCompareResult Compare(const rapidjson::Value& lhs, const rapidjson::Value& rhs);
//! Resolves all import directives, including nested imports, in the given document. An importer object needs to be passed
//! in through the settings.
//! @param jsonDoc The json document in which to resolve imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are resolved.
static JsonSerializationResult::ResultCode ResolveImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
//! Restores all import directives that were present in the json document. The same importer object that was
//! passed into ResolveImports through the settings needs to be passed here through settings as well.
//! @param jsonDoc The json document in which to restore imports.
//! @param allocator The allocator associated with the json document.
//! @param settings Additional settings that control the way the imports are restored.
static JsonSerializationResult::ResultCode RestoreImports(
rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings);
private:
JsonSerialization() = delete;
~JsonSerialization() = delete;
@@ -69,6 +69,9 @@ namespace AZ
case Tasks::CreatePatch:
target.append("a create patch operation ");
break;
case Tasks::Import:
target.append("an import operation");
break;
default:
target.append("an unknown operation ");
break;
@@ -32,7 +32,8 @@ namespace AZ
ReadField, //!< Task to read a field from JSON to a value.
WriteValue, //!< Task to write a value to a JSON field.
Merge, //!< Task to merge two JSON values/documents together.
CreatePatch //!< Task to create a patch to transform one value/document to another.
CreatePatch, //!< Task to create a patch to transform one value/document to another.
Import //!< Task to import a JSON document.
};
//! Describes how the task was processed.
+1 -1
View File
@@ -120,7 +120,7 @@ namespace AZ::Utils
AZ::Outcome<void, AZStd::string> WriteFile(AZStd::string_view content, AZStd::string_view filePath)
{
AZ::IO::FixedMaxPath filePathFixed = filePath; // Because FileIOStream requires a null-terminated string
AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite);
AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath);
bool success = false;
@@ -522,6 +522,8 @@ set(FILES
Serialization/Json/IntSerializer.cpp
Serialization/Json/JsonDeserializer.h
Serialization/Json/JsonDeserializer.cpp
Serialization/Json/JsonImporter.cpp
Serialization/Json/JsonImporter.h
Serialization/Json/JsonMerger.h
Serialization/Json/JsonMerger.cpp
Serialization/Json/JsonSerialization.h
@@ -0,0 +1,413 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Serialization/Json/JsonImporter.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <Tests/Serialization/Json/JsonSerializationTests.h>
namespace JsonSerializationTests
{
class JsonImportingTests;
class JsonImporterCustom
: public AZ::BaseJsonImporter
{
public:
AZ_RTTI(JsonImporterCustom, "{003F5896-71E0-4A50-A14F-08C319B06AD0}");
AZ::JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective,
const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) override;
JsonImporterCustom(JsonImportingTests* tests)
{
testClass = tests;
}
private:
JsonImportingTests* testClass;
};
class JsonImportingTests
: public BaseJsonSerializerFixture
{
public:
void SetUp() override
{
BaseJsonSerializerFixture::SetUp();
}
void TearDown() override
{
BaseJsonSerializerFixture::TearDown();
}
void GetTestDocument(const AZStd::string& docName, rapidjson::Document& out)
{
const char *objectJson = R"({
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
})";
const char *arrayJson = R"([
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
])";
const char *nestedImportJson = R"({
"desc" : "Nested Import",
"obj" : {"$import" : "object.json"}
})";
const char *nestedImportCycle1Json = R"({
"desc" : "Nested Import Cycle 1",
"obj" : {"$import" : "nested_import_c2.json"}
})";
const char *nestedImportCycle2Json = R"({
"desc" : "Nested Import Cycle 2",
"obj" : {"$import" : "nested_import_c1.json"}
})";
if (docName.compare("object.json") == 0)
{
out.Parse(objectJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("array.json") == 0)
{
out.Parse(arrayJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import.json") == 0)
{
out.Parse(nestedImportJson);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c1.json") == 0)
{
out.Parse(nestedImportCycle1Json);
ASSERT_FALSE(out.HasParseError());
}
else if (docName.compare("nested_import_c2.json") == 0)
{
out.Parse(nestedImportCycle2Json);
ASSERT_FALSE(out.HasParseError());
}
}
protected:
void TestImportLoadStore(const char* input, const char* expectedImportedValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
rapidjson::Document expectedOutcome;
expectedOutcome.Parse(expectedImportedValue);
ASSERT_FALSE(expectedOutcome.HasParseError());
TestResolveImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutcome.GetObject());
rapidjson::Document originalInput;
originalInput.Parse(input);
ASSERT_FALSE(originalInput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), originalInput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
void TestImportCycle(const char* input)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
AZ::JsonSerializationResult::ResultCode result = TestResolveImports(importerObj);
EXPECT_EQ(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Catastrophic);
m_jsonDocument->SetObject();
delete importerObj;
}
void TestInsertNewImport(const char* input, const char* expectedRestoredValue)
{
m_jsonDocument->Parse(input);
ASSERT_FALSE(m_jsonDocument->HasParseError());
JsonImporterCustom* importerObj = new JsonImporterCustom(this);
TestResolveImports(importerObj);
importerObj->AddImportDirective(rapidjson::Pointer("/object_2"), "object.json");
rapidjson::Document expectedOutput;
expectedOutput.Parse(expectedRestoredValue);
ASSERT_FALSE(expectedOutput.HasParseError());
TestRestoreImports(importerObj);
Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutput.GetObject());
m_jsonDocument->SetObject();
delete importerObj;
}
AZ::JsonSerializationResult::ResultCode TestResolveImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::ResolveImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
AZ::JsonSerializationResult::ResultCode TestRestoreImports(JsonImporterCustom* importerObj)
{
AZ::JsonImportSettings settings;
settings.m_importer = importerObj;
return AZ::JsonSerialization::RestoreImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings);
}
};
AZ::JsonSerializationResult::ResultCode JsonImporterCustom::ResolveImport(rapidjson::Value* importPtr,
rapidjson::Value& patch, const rapidjson::Value& importDirective, const AZ::IO::FixedMaxPath& importedFilePath,
rapidjson::Document::AllocatorType& allocator)
{
AZ::JsonSerializationResult::ResultCode resultCode(AZ::JsonSerializationResult::Tasks::Import);
rapidjson::Document importedDoc;
testClass->GetTestDocument(importedFilePath.String(), importedDoc);
if (importDirective.IsObject())
{
auto patchField = importDirective.FindMember("patch");
if (patchField != importDirective.MemberEnd())
{
patch.CopyFrom(patchField->value, allocator);
}
}
importPtr->CopyFrom(importedDoc, allocator);
return resultCode;
}
// Test Cases
TEST_F(JsonImportingTests, ImportSimpleObjectTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {"$import" : "object.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleObjectPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_2" : "patched_value" }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object": {
"field_1" : "value_1",
"field_2" : "patched_value",
"field_3" : "value_3"
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {"$import" : "array.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "value_1" },
{ "element_2" : "value_2" },
{ "element_3" : "value_3" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, ImportSimpleArrayPatchTest)
{
const char* inputFile = R"(
{
"name" : "simple_array_import",
"object": {
"$import" : {
"filename" : "array.json",
"patch" : [ { "element_1" : "patched_value" } ]
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_array_import",
"object": [
{ "element_1" : "patched_value" }
]
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {"$import" : "nested_import.json"}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportPatchTest)
{
const char* inputFile = R"(
{
"name" : "nested_import",
"object": {
"$import" : {
"filename" : "nested_import.json",
"patch" : { "obj" : { "field_3" : "patched_value" } }
}
}
}
)";
const char* expectedOutput = R"(
{
"name" : "nested_import",
"object": {
"desc" : "Nested Import",
"obj" : {
"field_1" : "value_1",
"field_2" : "value_2",
"field_3" : "patched_value"
}
}
}
)";
TestImportLoadStore(inputFile, expectedOutput);
}
TEST_F(JsonImportingTests, NestedImportCycleTest)
{
const char* inputFile = R"(
{
"name" : "nested_import_cycle",
"object": {"$import" : "nested_import_c1.json"}
}
)";
TestImportCycle(inputFile);
}
TEST_F(JsonImportingTests, InsertNewImportTest)
{
const char* inputFile = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"field_1" : "other_value",
"field_2" : "value_2",
"field_3" : "value_3"
}
}
)";
const char* expectedOutput = R"(
{
"name" : "simple_object_import",
"object_1": {"$import" : "object.json"},
"object_2": {
"$import" : {
"filename" : "object.json",
"patch" : { "field_1" : "other_value" }
}
}
}
)";
TestInsertNewImport(inputFile, expectedOutput);
}
}
@@ -121,6 +121,7 @@ set(FILES
Serialization/Json/TestCases_Classes.cpp
Serialization/Json/TestCases_Compare.cpp
Serialization/Json/TestCases_Enum.cpp
Serialization/Json/TestCases_Importing.cpp
Serialization/Json/TestCases_Patching.cpp
Serialization/Json/TestCases_Pointers.h
Serialization/Json/TestCases_Pointers.cpp
@@ -22,7 +22,7 @@ namespace AzFramework
AZStd::scoped_ptr<ProcessWatcher> pWatcher(LaunchProcess(processLaunchInfo, communicationType));
if (!pWatcher)
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process '%s %s'\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -31,7 +31,7 @@ namespace AzFramework
ProcessCommunicator* pCommunicator = pWatcher->GetCommunicator();
if (!pCommunicator || !pCommunicator->IsValid())
{
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: No communicator for watcher's process (%s %s)!\n", processLaunchInfo.m_processExecutableString.c_str(), processLaunchInfo.m_commandlineParameters.c_str());
return false;
}
else
@@ -14,9 +14,8 @@ namespace AzFramework
{
AZ_CVAR(bool, bg_octreeUseQuadtree, false, nullptr, AZ::ConsoleFunctorFlags::ReadOnly, "If set to true, the visibility octrees will degenerate to a quadtree split along the X/Y plane");
AZ_CVAR(float, bg_octreeMaxWorldExtents, 16384.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum supported world size by the world octreeSystemComponent");
AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split");
AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation");
AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split");
AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation");
static uint32_t GetChildNodeCount()
{
@@ -25,14 +24,12 @@ namespace AzFramework
return (bg_octreeUseQuadtree) ? QuadtreeNodeChildCount : OctreeNodeChildCount;
}
OctreeNode::OctreeNode(const AZ::Aabb& bounds)
: m_bounds(bounds)
{
;
}
OctreeNode::OctreeNode(OctreeNode&& rhs)
: m_bounds(rhs.m_bounds)
, m_parent(rhs.m_parent)
@@ -46,7 +43,6 @@ namespace AzFramework
}
}
OctreeNode& OctreeNode::operator=(OctreeNode&& rhs)
{
m_bounds = rhs.m_bounds;
@@ -63,7 +59,6 @@ namespace AzFramework
return *this;
}
void OctreeNode::Insert(OctreeScene& octreeScene, VisibilityEntry* entry)
{
AZ_Assert(entry->m_internalNode == nullptr, "Double-insertion: Insert invoked for an entry already bound to the OctreeScene");
@@ -98,7 +93,6 @@ namespace AzFramework
}
}
void OctreeNode::Update(OctreeScene& octreeScene, VisibilityEntry* entry)
{
AZ_Assert(entry->m_internalNode == this, "Update invoked for an entry bound to a different OctreeNode");
@@ -129,7 +123,6 @@ namespace AzFramework
}
}
void OctreeNode::Remove(OctreeScene& octreeScene, VisibilityEntry* entry)
{
AZ_Assert(entry->m_internalNode == this, "Remove invoked for an entry bound to a different OctreeNode");
@@ -152,25 +145,30 @@ namespace AzFramework
}
}
void OctreeNode::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const
{
EnumerateHelper(aabb, callback);
if (AZ::ShapeIntersection::Overlaps(aabb, m_bounds))
{
EnumerateHelper(aabb, callback);
}
}
void OctreeNode::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const
{
EnumerateHelper(sphere, callback);
if (AZ::ShapeIntersection::Overlaps(sphere, m_bounds))
{
EnumerateHelper(sphere, callback);
}
}
void OctreeNode::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const
{
EnumerateHelper(frustum, callback);
if (AZ::ShapeIntersection::Overlaps(frustum, m_bounds))
{
EnumerateHelper(frustum, callback);
}
}
void OctreeNode::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const
{
// Invoke the callback for the current node
@@ -190,25 +188,21 @@ namespace AzFramework
}
}
const AZStd::vector<VisibilityEntry*>& OctreeNode::GetEntries() const
{
return m_entries;
}
OctreeNode* OctreeNode::GetChildren() const
{
return m_children;
}
bool OctreeNode::IsLeaf() const
{
return m_children == nullptr;
}
void OctreeNode::TryMerge(OctreeScene& octreeScene)
{
if (IsLeaf())
@@ -236,7 +230,6 @@ namespace AzFramework
}
}
template <typename T>
void OctreeNode::EnumerateHelper(const T& boundingVolume, const IVisibilityScene::EnumerateCallback& callback) const
{
@@ -262,7 +255,6 @@ namespace AzFramework
}
}
void OctreeNode::Split(OctreeScene& octreeScene)
{
AZ_Assert(m_children == nullptr, "Split invoked on an octreeScene node that has already been split");
@@ -312,7 +304,6 @@ namespace AzFramework
}
}
void OctreeNode::Merge(OctreeScene& octreeScene)
{
AZ_Assert(m_children != nullptr, "Merge invoked on an octreeScene node that does not have children");
@@ -371,7 +362,6 @@ namespace AzFramework
}
}
void OctreeScene::RemoveEntry(VisibilityEntry& entry)
{
AZStd::lock_guard<AZStd::shared_mutex> lock(m_sharedMutex);
@@ -382,35 +372,30 @@ namespace AzFramework
}
}
void OctreeScene::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.Enumerate(aabb, callback);
}
void OctreeScene::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.Enumerate(sphere, callback);
}
void OctreeScene::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.Enumerate(frustum, callback);
}
void OctreeScene::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const
{
AZStd::shared_lock<AZStd::shared_mutex> lock(m_sharedMutex);
m_root.EnumerateNoCull(callback);
}
uint32_t OctreeScene::GetEntryCount() const
{
return m_entryCount;
@@ -421,26 +406,22 @@ namespace AzFramework
return m_nodeCount;
}
uint32_t OctreeScene::GetFreeNodeCount() const
{
// Each entry represents GetChildNodeCount() nodes
return aznumeric_cast<uint32_t>(m_freeOctreeNodes.size() * GetChildNodeCount());
}
uint32_t OctreeScene::GetPageCount() const
{
return aznumeric_cast<uint32_t>(m_nodeCache.size());
}
uint32_t OctreeScene::GetChildNodeCount() const
{
return AzFramework::GetChildNodeCount();
}
void OctreeScene::DumpStats()
{
AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::EntryCount = %u", GetName().GetCStr(), GetEntryCount());
@@ -450,21 +431,18 @@ namespace AzFramework
AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::ChildNodeCount = %u", GetName().GetCStr(), GetChildNodeCount());
}
static inline uint32_t CreateNodeIndex(uint32_t page, uint32_t offset)
{
AZ_Assert(page <= 0xFFFF && offset <= 0xFFFF, "Out of range values passed to CreateNodeIndex");
return (page << 16) | offset;
}
static inline void ExtractPageAndOffsetFromIndex(uint32_t index, uint32_t& page, uint32_t& offset)
{
offset = index & 0x0000FFFF;
page = index >> 16;
}
uint32_t OctreeScene::AllocateChildNodes()
{
const uint32_t childCount = GetChildNodeCount();
@@ -508,14 +486,12 @@ namespace AzFramework
return CreateNodeIndex(nextChildPage, nextChildOffset);
}
void OctreeScene::ReleaseChildNodes(uint32_t nodeIndex)
{
m_nodeCount -= GetChildNodeCount();
m_freeOctreeNodes.push(nodeIndex);
}
OctreeNode* OctreeScene::GetChildNodesAtIndex(uint32_t nodeIndex) const
{
uint32_t childPage;
@@ -524,7 +500,6 @@ namespace AzFramework
return &(*m_nodeCache[childPage])[childOffset];
}
void OctreeSystemComponent::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
@@ -534,19 +509,16 @@ namespace AzFramework
}
}
void OctreeSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
{
provided.push_back(AZ_CRC("OctreeService"));
}
void OctreeSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
{
incompatible.push_back(AZ_CRC("OctreeService"));
}
OctreeSystemComponent::OctreeSystemComponent()
{
AZ::Interface<IVisibilitySystem>::Register(this);
@@ -555,7 +527,6 @@ namespace AzFramework
m_defaultScene = aznew OctreeScene(AZ::Name("DefaultVisibilityScene"));
}
OctreeSystemComponent::~OctreeSystemComponent()
{
AZ_Assert(m_scenes.empty(), "All IVisibilityScenes must be destroyed before shutdown");
@@ -566,13 +537,11 @@ namespace AzFramework
AZ::Interface<IVisibilitySystem>::Unregister(this);
}
void OctreeSystemComponent::Activate()
{
;
}
void OctreeSystemComponent::Deactivate()
{
;
@@ -591,7 +560,6 @@ namespace AzFramework
return newScene;
}
void OctreeSystemComponent::DestroyVisibilityScene(IVisibilityScene* visScene)
{
for (auto iter = m_scenes.begin(); iter != m_scenes.end(); ++iter)
@@ -606,7 +574,6 @@ namespace AzFramework
AZ_Assert(false, "visScene[\"%s\"] not found in the OctreeSystemComponent", visScene->GetName().GetCStr());
}
IVisibilityScene* OctreeSystemComponent::FindVisibilityScene(const AZ::Name& sceneName)
{
for (OctreeScene* scene : m_scenes)
@@ -619,7 +586,6 @@ namespace AzFramework
return nullptr;
}
void OctreeSystemComponent::DumpStats([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments)
{
for (OctreeScene* scene : m_scenes)
@@ -7,17 +7,35 @@
*/
#include <AzFramework/Application/Application.h>
#include <sys/resource.h>
#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
#include <AzFramework/XcbApplication.h>
#endif
constexpr rlim_t g_minimumOpenFileHandles = 65536L;
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
Application::Implementation* Application::Implementation::Create()
{
// The default open file limit for processes may not be enough for O3DE applications.
// We will need to increase to the recommended value if the current open file limit
// is not sufficient.
rlimit currentLimit;
int get_limit_result = getrlimit(RLIMIT_NOFILE, &currentLimit);
AZ_Warning("Application", get_limit_result == 0, "Unable to read current ulimit open file limits");
if ((get_limit_result == 0) && (currentLimit.rlim_cur < g_minimumOpenFileHandles || currentLimit.rlim_max < g_minimumOpenFileHandles))
{
rlimit newLimit;
newLimit.rlim_cur = g_minimumOpenFileHandles; // Soft Limit
newLimit.rlim_max = g_minimumOpenFileHandles; // Hard Limit
[[maybe_unused]] int set_limit_result = setrlimit(RLIMIT_NOFILE, &newLimit);
AZ_Assert(set_limit_result == 0, "Unable to update open file limits");
}
#if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
return aznew XcbApplication();
#elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND
@@ -91,31 +91,42 @@ namespace AzFramework
return processId == 0;
}
/*! Executes a command in the child process after the fork operation has been executed.
* This function will never return. If the execvp command fails this will call _exit with
* the errno value as the return value since continuing execution after a execvp command
* is invalid (it will be running the parent's code and in its address space and will
* cause many issues).
/*! Executes a command in the child process after the fork operation
* has been executed. This function will never return. If the execvpe
* command fails this will call _exit since continuing execution after
* a execvpe command is invalid (it will be running the parent's code
* and in its address space and will cause many issues).
*
* This function runs after a `fork()` call. `fork()` creates a copy of
* the current process, including the current state of the process's
* memory, at the time the call is made. However, it only creates a
* copy of the one thread that called `fork()`. This means that if any
* mutexes are locked by other threads at the time of `fork()`, those
* mutexes will remain locked in the child process, with no way to
* unlock them. So this function needs to ensure that it does as little
* work as possible.
*
* \param commandAndArgs - Array of strings that has the command to execute in index 0 with any args for the command following. Last element must be a null pointer.
* \param envionrmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param environmentVariables - Array of strings that contains environment variables that command should use. Last element must be a null pointer.
* \param processLaunchInfo - struct containing information about luanching the command
* \param startupInfo - struct containing information needed to startup the command
* \param errorPipe - a pipe file descriptor used to communicate a failed execvpe call's error code to the parent process
*/
void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo)
[[noreturn]] static void ExecuteCommandAsChild(char** commandAndArgs, char** environmentVariables, const ProcessLauncher::ProcessLaunchInfo& processLaunchInfo, StartupInfo& startupInfo, const AZStd::array<int, 2>& errorPipe)
{
close(errorPipe[0]);
if (!processLaunchInfo.m_workingDirectory.empty())
{
int res = chdir(processLaunchInfo.m_workingDirectory.c_str());
if (res != 0)
{
std::cerr << strerror(errno) << std::endl;
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to change the launched process' directory to '%s'.", processLaunchInfo.m_workingDirectory.c_str());
write(errorPipe[1], &errno, sizeof(int));
// We *have* to _exit as we are the child process and simply
// returning at this point would mean we would start running
// the code from our parent process and that will just wreck
// havoc.
_exit(errno);
_exit(0);
}
}
@@ -135,15 +146,17 @@ namespace AzFramework
startupInfo.SetupHandlesForChildProcess();
execve(commandAndArgs[0], commandAndArgs, environmentVariables);
execvpe(commandAndArgs[0], commandAndArgs, environmentVariables);
const int errval = errno;
// If we get here then execve failed to run the requested program and
// If we get here then execvpe failed to run the requested program and
// we have an error. In this case we need to exit the child process
// to stop it from continuing to run as a clone of the parent
AZ_TracePrintf("Process Watcher", "ProcessWatcher::LaunchProcessAndRetrieveOutput: Unable to launch process %s : errno = %s ", commandAndArgs[0], strerror(errno));
std::cerr << strerror(errno) << std::endl;
// to stop it from continuing to run as a clone of the parent.
// Communicate the error code back to the parent via a pipe for the
// parent to read.
write(errorPipe[1], &errval, sizeof(errval));
_exit(errno);
_exit(0);
}
}
@@ -212,9 +225,8 @@ namespace AzFramework
AZStd::string outputString;
bool inQuotes = false;
for (size_t pos = 0; pos < processLaunchInfo.m_commandlineParameters.size(); ++pos)
for (const char currentChar : processLaunchInfo.m_commandlineParameters)
{
char currentChar = processLaunchInfo.m_commandlineParameters[pos];
if (currentChar == '"')
{
inQuotes = !inQuotes;
@@ -231,7 +243,7 @@ namespace AzFramework
outputString.push_back(currentChar);
}
}
if (!outputString.empty())
{
commandTokens.push_back(outputString);
@@ -249,10 +261,10 @@ namespace AzFramework
return false;
}
// Because of the way execve is defined we need to copy the strings from
// Because of the way execvpe is defined we need to copy the strings from
// AZ::string (using c_str() returns a const char*) into a non-const char*
// Need to add one more as exec requires the array's last element to be a null pointer
// Need to add one more as execvpe requires the array's last element to be a null pointer
char** commandAndArgs = new char*[commandTokens.size() + 1];
for (int i = 0; i < commandTokens.size(); ++i)
{
@@ -275,7 +287,7 @@ namespace AzFramework
azstrcat(environmentVariable.get(), envVarString.size() + 1, envVarString.c_str());
environmentVariablesVector.emplace_back(environmentVariable.get());
}
// Adding one more as exec expects the array to have a nullptr as the last element
// Adding one more as execvpe expects the array to have a nullptr as the last element
environmentVariablesVector.emplace_back(nullptr);
environmentVariables = environmentVariablesVector.data();
}
@@ -288,15 +300,50 @@ namespace AzFramework
AZ_Assert(environmentVariables, "Environment variables for current process not available\n");
}
// Set up a pipe to communicate the error code from the subprocess's execvpe call
AZStd::array<int, 2> childErrorPipeFds{};
pipe(childErrorPipeFds.data());
// This configures the write end of the pipe to close on calls to `exec`
fcntl(childErrorPipeFds[1], F_SETFD, fcntl(childErrorPipeFds[1], F_GETFD) | FD_CLOEXEC);
pid_t child_pid = fork();
if (IsIdChildProcess(child_pid))
{
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo);
ExecuteCommandAsChild(commandAndArgs, environmentVariables, processLaunchInfo, processData.m_startupInfo, childErrorPipeFds);
}
processData.m_childProcessId = child_pid;
// Close these handles as they are only to be used by the child process
processData.m_startupInfo.CloseAllHandles();
close(childErrorPipeFds[1]);
{
int errorCodeFromChild = 0;
int count = 0;
// Read from the error pipe.
// * If the child's call to execvpe succeeded, then the pipe will
// be closed due to setting FD_CLOEXEC on the write end of the
// pipe. `read()` will return 0.
// * If the child's call to execvpe failed, the child will have
// written the error code to the pipe. `read()` will return >0, and
// the data to be read is the error code from execvpe.
while ((count = read(childErrorPipeFds[0], &errorCodeFromChild, sizeof(errorCodeFromChild))) == -1)
{
if (errno != EAGAIN && errno != EINTR)
{
break;
}
}
if (count)
{
AZ_TracePrintf("Process Watcher", "ProcessLauncher::LaunchProcess: Unable to launch process %s : errno = %s\n", commandAndArgs[0], strerror(errorCodeFromChild));
processData.m_childProcessIsDone = true;
child_pid = -1;
}
}
close(childErrorPipeFds[0]);
processData.m_childProcessId = child_pid;
for (int i = 0; i < commandTokens.size(); i++)
{
@@ -53,7 +53,7 @@ namespace AzNetworking
m_timeoutItemMap.erase(timeoutId);
}
void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts)
void TimeoutQueue::UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts)
{
int32_t numTimeouts = 0;
if (maxTimeouts < 0)
@@ -103,7 +103,7 @@ namespace AzNetworking
// By this point, the item is definitely timed out
// Invoke the timeout function to see how to proceed
const TimeoutResult result = timeoutHandler.HandleTimeout(mapItem);
const TimeoutResult result = timeoutHandler(mapItem);
if (result == TimeoutResult::Refresh)
{
@@ -122,4 +122,10 @@ namespace AzNetworking
m_timeoutItemMap.erase(itemTimeoutId);
}
}
void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts)
{
TimeoutHandler handler([&timeoutHandler](TimeoutQueue::TimeoutItem& item) { return timeoutHandler.HandleTimeout(item); });
UpdateTimeouts(handler, maxTimeouts);
}
}
@@ -64,6 +64,12 @@ namespace AzNetworking
//! @param timeoutId the identifier of the item to remove
void RemoveItem(TimeoutId timeoutId);
//! Updates timeouts for all items, invokes the provided timeout functor if required.
//! @param timeoutHandler lambda to invoke for all timeouts
//! @param maxTimeouts the maximum number of timeouts to process before breaking iteration
using TimeoutHandler = AZStd::function<TimeoutResult(TimeoutQueue::TimeoutItem&)>;
void UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts = -1);
//! Updates timeouts for all items, invokes timeout handlers if required.
//! @param timeoutHandler listener instance to call back on for timeouts
//! @param maxTimeouts the maximum number of timeouts to process before breaking iteration
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.38947 2H2.97474C1.61046 2 0.5 3.09717 0.5 4.44876V13.0353C0.5 14.4028 1.61046 15.5 2.97474 15.5H11.5253C12.8895 15.5 14 14.4028 14 13.0353V9.61053C13.5767 9.88652 13.1131 10.1058 12.6199 10.2576V13.0353H12.5881C12.5881 13.6396 12.0964 14.1007 11.5094 14.1007H2.97474C2.37192 14.1007 1.896 13.6237 1.896 13.0353V4.44876C1.896 3.86042 2.38778 3.38339 2.97474 3.38339H5.74142C5.89324 2.88897 6.11287 2.42422 6.38947 2Z" fill="white"/>
<path d="M11 0.5C8.51446 0.5 6.5 2.51471 6.5 5C6.5 7.48529 8.51446 9.5 11 9.5C13.485 9.5 15.5 7.48529 15.5 5C15.5 2.51471 13.485 0.5 11 0.5ZM13.8633 6.39526C13.9155 6.43923 13.8975 6.54774 13.8221 6.63723L13.1024 7.49454C13.0276 7.58429 12.9237 7.62106 12.8715 7.57708L11.0003 6.00697L9.12903 7.57683C9.07683 7.62106 8.97346 7.58403 8.89811 7.49454L8.17837 6.63723C8.10354 6.54749 8.08503 6.43897 8.13723 6.39526L9.80017 4.99974L8.13723 3.60449C8.08503 3.56026 8.10303 3.452 8.17837 3.36251L8.8976 2.5052C8.97294 2.4152 9.07631 2.37869 9.12903 2.42266L11.0003 3.99277L12.8715 2.42266C12.9242 2.37869 13.0276 2.41546 13.1029 2.5052L13.8221 3.36251C13.8975 3.452 13.9155 3.56051 13.8633 3.60449L12.2003 5L13.8633 6.39526Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -6,6 +6,8 @@
<file alias="layer.svg">Entity/layer.svg</file>
<file alias="prefab.svg">Entity/prefab.svg</file>
<file alias="prefab_edit.svg">Entity/prefab_edit.svg</file>
<file alias="prefab_edit_open.svg">Entity/prefab_edit_open.svg</file>
<file alias="prefab_edit_close.svg">Entity/prefab_edit_close.svg</file>
</qresource>
<qresource prefix="/Level">
<file alias="level.svg">Level/level.svg</file>
@@ -7,6 +7,7 @@
*/
#include <dlfcn.h>
#include <iostream>
#include <AzCore/IO/Path/Path.h>
#include <AzTest/Platform.h>
#include <sys/types.h>
@@ -20,11 +21,16 @@ public:
explicit ModuleHandle(const std::string& lib)
: m_libHandle(nullptr)
{
std::string libext = lib;
if (!AZ::Test::EndsWith(libext, ".dylib"))
AZ::IO::FixedMaxPath libext = AZStd::string_view{ lib.c_str(), lib.size() };
if (!libext.Stem().Native().starts_with(AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX))
{
libext += ".dylib";
libext = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX + libext.Native();
}
if (libext.Extension() != AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION)
{
libext.Native() += AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
}
m_libHandle = dlopen(libext.c_str(), RTLD_NOW);
const char* error = dlerror();
if (error)
@@ -9,6 +9,8 @@
#include <AzToolsFramework/Prefab/EditorPrefabComponent.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Uuid.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/Prefab/PrefabPublicInterface.h>
@@ -38,6 +40,13 @@ namespace AzToolsFramework
AZ::Edit::SliceFlags::DontGatherReference);
}
}
if (auto behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context))
{
behaviorContext->ConstantProperty(
"EditorPrefabComponentTypeId", BehaviorConstant(AZ::Uuid(EditorPrefabComponent::EditorPrefabComponentTypeId)))
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation);
}
}
void EditorPrefabComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services)
@@ -16,7 +16,9 @@ namespace AzToolsFramework
class EditorPrefabComponent : public AzToolsFramework::Components::EditorComponentBase
{
public:
AZ_COMPONENT(EditorPrefabComponent, "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}", EditorComponentBase);
static constexpr const char* const EditorPrefabComponentTypeId = "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}";
AZ_COMPONENT(EditorPrefabComponent, EditorPrefabComponentTypeId, EditorComponentBase);
static void Reflect(AZ::ReflectContext* context);
static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services);
@@ -206,6 +206,34 @@ namespace AzToolsFramework::Prefab
return instance.has_value() && (&instance->get() == &m_focusedInstance->get());
}
bool PrefabFocusHandler::IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const
{
if (!m_focusedInstance.has_value())
{
// PrefabFocusHandler has not been initialized yet.
return false;
}
if (!entityId.IsValid())
{
return false;
}
InstanceOptionalReference instance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
while (instance.has_value())
{
if (&instance->get() == &m_focusedInstance->get())
{
return true;
}
instance = instance->get().GetParentInstance();
}
return false;
}
const AZ::IO::Path& PrefabFocusHandler::GetPrefabFocusPath([[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
return m_instanceFocusPath;
@@ -53,6 +53,7 @@ namespace AzToolsFramework::Prefab
PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override;
AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const override;
bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const override;
bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const override;
const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const override;
const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override;
@@ -37,10 +37,15 @@ namespace AzToolsFramework::Prefab
//! Returns the entity id of the container entity for the instance the prefab system is focusing on.
virtual AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const = 0;
//! Returns whether the entity belongs to the instance that is being focused on.
//! @param entityId The entityId of the queried entity.
//! @return true if the entity belongs to the focused instance, false otherwise.
virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0;
//! Returns whether the entity belongs to the instance that is being focused on, or one of its descendants.
//! @param entityId The entityId of the queried entity.
//! @return true if the entity belongs to the focused instance or one of its descendants, false otherwise.
virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0;
virtual bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const = 0;
//! Returns the path from the root instance to the currently focused instance.
//! @return A path composed from the names of the container entities for the instance path.
@@ -974,7 +974,7 @@ namespace AzToolsFramework
return DeleteFromInstance(entityIds, true);
}
PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
DuplicatePrefabResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
{
if (entityIds.empty())
{
@@ -1021,6 +1021,7 @@ namespace AzToolsFramework
ScopedUndoBatch undoBatch("Duplicate Entities");
EntityIdList duplicatedEntityAndInstanceIds;
{
AZ_PROFILE_SCOPE(AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities");
@@ -1033,7 +1034,7 @@ namespace AzToolsFramework
if (!retrieveEntitiesAndInstancesOutcome.IsSuccess())
{
return AZStd::move(retrieveEntitiesAndInstancesOutcome);
return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError());
}
// Take a snapshot of the instance DOM before we manipulate it
@@ -1044,8 +1045,6 @@ namespace AzToolsFramework
PrefabDom instanceDomAfter;
instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator());
EntityIdList duplicatedEntityAndInstanceIds;
// Duplicate any nested entities and instances as requested
AZStd::unordered_map<InstanceAlias, Instance*> newInstanceAliasToOldInstanceMap;
AZStd::unordered_map<EntityAlias, EntityAlias> duplicateEntityAliasMap;
@@ -1114,7 +1113,7 @@ namespace AzToolsFramework
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds);
}
return AZ::Success();
return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds));
}
PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants)
@@ -63,7 +63,7 @@ namespace AzToolsFramework
PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override;
@@ -26,6 +26,7 @@ namespace AzToolsFramework
{
typedef AZ::Outcome<AZ::EntityId, AZStd::string> CreatePrefabResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> InstantiatePrefabResult;
typedef AZ::Outcome<EntityIdList, AZStd::string> DuplicatePrefabResult;
typedef AZ::Outcome<void, AZStd::string> PrefabOperationResult;
typedef AZ::Outcome<bool, AZStd::string> PrefabRequestResult;
typedef AZ::Outcome<AZ::EntityId, AZStd::string> PrefabEntityResult;
@@ -160,14 +161,15 @@ namespace AzToolsFramework
/**
* Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance.
* @param entities The entities to duplicate.
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
* @return An outcome object with a list of ids of target entities' duplicates if duplication succeeded;
* on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
/**
* If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting
* the container entity into a regular entity and putting it under the parent prefab, removing the link between this
* instance and the parent, removing links between this instance and it's nested instances, adding entities directly
* instance and the parent, removing links between this instance and its nested instances, and adding entities directly
* owned by this instance under the parent instance.
* Bails if the entity is not a container entity or belongs to the level prefab instance.
* @param containerEntityId The container entity id of the instance to detach.
@@ -25,6 +25,7 @@ namespace AzToolsFramework
{
using CreatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using InstantiatePrefabResult = AZ::Outcome<AZ::EntityId, AZStd::string>;
using DuplicatePrefabResult = AZ::Outcome<EntityIdList, AZStd::string>;
using PrefabOperationResult = AZ::Outcome<void, AZStd::string>;
/**
@@ -69,6 +70,29 @@ namespace AzToolsFramework
* Return an outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0;
/**
* If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting
* the container entity into a regular entity and putting it under the parent prefab, removing the link between this
* instance and the parent, removing links between this instance and its nested instances, and adding entities directly
* owned by this instance under the parent instance.
* Bails if the entity is not a container entity or belongs to the level prefab instance.
* Return an outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0;
/**
* Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance.
* Return an outcome object with a list of ids of given entities' duplicates if duplication succeeded;
* on failure, it comes with an error message detailing the cause of the error.
*/
virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
/**
* Get the file path to the prefab file for the prefab instance owning the entity provided.
* Returns the path to the prefab, or an empty path if the entity is owned by the level.
*/
virtual AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const = 0;
};
using PrefabPublicRequestBus = AZ::EBus<PrefabPublicRequests>;
@@ -28,6 +28,9 @@ namespace AzToolsFramework
->Event("CreatePrefabInMemory", &PrefabPublicRequests::CreatePrefabInMemory)
->Event("InstantiatePrefab", &PrefabPublicRequests::InstantiatePrefab)
->Event("DeleteEntitiesAndAllDescendantsInInstance", &PrefabPublicRequests::DeleteEntitiesAndAllDescendantsInInstance)
->Event("DetachPrefab", &PrefabPublicRequests::DetachPrefab)
->Event("DuplicateEntitiesInInstance", &PrefabPublicRequests::DuplicateEntitiesInInstance)
->Event("GetOwningInstancePrefabPath", &PrefabPublicRequests::GetOwningInstancePrefabPath)
;
}
}
@@ -62,5 +65,19 @@ namespace AzToolsFramework
return m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entityIds);
}
PrefabOperationResult PrefabPublicRequestHandler::DetachPrefab(const AZ::EntityId& containerEntityId)
{
return m_prefabPublicInterface->DetachPrefab(containerEntityId);
}
DuplicatePrefabResult PrefabPublicRequestHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
{
return m_prefabPublicInterface->DuplicateEntitiesInInstance(entityIds);
}
AZStd::string PrefabPublicRequestHandler::GetOwningInstancePrefabPath(AZ::EntityId entityId) const
{
return m_prefabPublicInterface->GetOwningInstancePrefabPath(entityId).Native();
}
} // namespace Prefab
} // namespace AzToolsFramework
@@ -34,6 +34,9 @@ namespace AzToolsFramework
CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override;
InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override;
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override;
DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const override;
private:
PrefabPublicInterface* m_prefabPublicInterface = nullptr;
@@ -23,7 +23,7 @@ namespace AzToolsFramework
}
//PrefabInstanceUndo
PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation)
PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation)
: PrefabUndoBase(undoOperationName)
{
m_useImmediatePropagation = useImmediatePropagation;
@@ -45,7 +45,7 @@ namespace AzToolsFramework
: public PrefabUndoBase
{
public:
explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true);
explicit PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation = true);
void Capture(
const PrefabDom& initialState,
@@ -2648,16 +2648,15 @@ namespace AzToolsFramework
}
else
{
QString cleanSaveAs(QDir::cleanPath(slicePath));
AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(slicePath.toUtf8().constData()).LexicallyNormal();
bool isPathSafeForAssets = false;
for (AZStd::string assetSafeFolder : assetSafeFolders)
for (const AZStd::string& assetSafeFolder : assetSafeFolders)
{
QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str()));
// Compare using clean paths so slash direction does not matter.
// Note that this comparison is case sensitive because some file systems
// Open 3D Engine supports are case sensitive.
if (cleanSaveAs.startsWith(cleanAssetSafeFolder))
AZ::IO::PathView assetSafeFolderView(assetSafeFolder);
// Check if the slice path is relative to the safe asset directory.
// The Path classes are being used to make this check case insensitive.
if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView))
{
isPathSafeForAssets = true;
break;
@@ -101,8 +101,25 @@ namespace AzToolsFramework
{
}
void EditorEntityUiHandlerBase::OnDoubleClick([[maybe_unused]] AZ::EntityId entityId) const
bool EditorEntityUiHandlerBase::OnOutlinerItemClick(
[[maybe_unused]] const QPoint& position,
[[maybe_unused]] const QStyleOptionViewItem& option,
[[maybe_unused]] const QModelIndex& index) const
{
return false;
}
void EditorEntityUiHandlerBase::OnOutlinerItemExpand([[maybe_unused]] const QModelIndex& index) const
{
}
void EditorEntityUiHandlerBase::OnOutlinerItemCollapse([[maybe_unused]] const QModelIndex& index) const
{
}
bool EditorEntityUiHandlerBase::OnEntityDoubleClick([[maybe_unused]] AZ::EntityId entityId) const
{
return false;
}
} // namespace AzToolsFramework
@@ -61,8 +61,17 @@ namespace AzToolsFramework
virtual void PaintDescendantForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index,
const QModelIndex& descendantIndex) const;
//! Triggered when the entity is double clicked in the Outliner.
virtual void OnDoubleClick(AZ::EntityId entityId) const;
//! Triggered when the entity is clicked in the Outliner.
//! @return True if the click has been handled and should not be propagated, false otherwise.
virtual bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const;
//! Triggered when an entity's children are expanded in the Outliner.
virtual void OnOutlinerItemExpand(const QModelIndex& index) const;
//! Triggered when an entity's children are collapsed in the Outliner.
virtual void OnOutlinerItemCollapse(const QModelIndex& index) const;
//! Triggered when the entity is double clicked in the Outliner or in the Viewport.
//! @return True if the double click has been handled and should not be propagated, false otherwise.
virtual bool OnEntityDoubleClick(AZ::EntityId entityId) const;
private:
EditorEntityUiHandlerId m_handlerId = 0;
@@ -11,10 +11,12 @@
#include <QApplication>
#include <QBitmap>
#include <QCheckBox>
#include <QEvent>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QMessageBox>
#include <QMimeData>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QStyle>
@@ -2287,7 +2289,14 @@ namespace AzToolsFramework
// Now we setup a Text Document so it can draw the rich text
QTextDocument textDoc;
textDoc.setDefaultFont(optionV4.font);
textDoc.setDefaultStyleSheet("body {color: white}");
if (option.state & QStyle::State_Enabled)
{
textDoc.setDefaultStyleSheet("body {color: white}");
}
else
{
textDoc.setDefaultStyleSheet("body {color: #7C7C7C}");
}
textDoc.setHtml("<body>" + entityNameRichText + "</body>");
painter->translate(textRect.topLeft());
textDoc.setTextWidth(textRect.width());
@@ -2326,6 +2335,23 @@ namespace AzToolsFramework
return true;
}
if (event->type() == QEvent::MouseButtonPress)
{
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
if (auto editorEntityUiInterface = AZ::Interface<EditorEntityUiInterface>::Get(); editorEntityUiInterface != nullptr)
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
auto entityUiHandler = editorEntityUiInterface->GetHandler(entityId);
if (entityUiHandler && entityUiHandler->OnOutlinerItemClick(mouseEvent->pos(), option, index))
{
return true;
}
}
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
@@ -73,6 +73,8 @@ namespace AzToolsFramework
void EntityOutlinerTreeView::leaveEvent([[maybe_unused]] QEvent* event)
{
m_mousePosition = QPoint();
m_currentHoveredIndex = QModelIndex();
update();
}
void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event)
@@ -129,6 +131,11 @@ namespace AzToolsFramework
}
m_mousePosition = event->pos();
if (QModelIndex hoveredIndex = indexAt(m_mousePosition); m_currentHoveredIndex != indexAt(m_mousePosition))
{
m_currentHoveredIndex = hoveredIndex;
update();
}
//process mouse movement as normal, potentially triggering drag and drop
QTreeView::mouseMoveEvent(event);
@@ -90,6 +90,8 @@ namespace AzToolsFramework
const QColor m_selectedColor = QColor(255, 255, 255, 45);
const QColor m_hoverColor = QColor(255, 255, 255, 30);
QModelIndex m_currentHoveredIndex;
EditorEntityUiInterface* m_editorEntityFrameworkInterface;
};
@@ -902,6 +902,7 @@ namespace AzToolsFramework
EditorPickModeRequestBus::Broadcast(
&EditorPickModeRequests::StopEntityPickMode);
return;
}
switch (index.column())
@@ -918,18 +919,30 @@ namespace AzToolsFramework
{
if (AZ::EntityId entityId = GetEntityIdFromIndex(index); auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId))
{
entityUiHandler->OnDoubleClick(entityId);
entityUiHandler->OnEntityDoubleClick(entityId);
}
}
void EntityOutlinerWidget::OnTreeItemExpanded(const QModelIndex& index)
{
m_listModel->OnEntityExpanded(GetEntityIdFromIndex(index));
AZ::EntityId entityId = GetEntityIdFromIndex(index);
if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId))
{
entityUiHandler->OnOutlinerItemExpand(index);
}
m_listModel->OnEntityExpanded(entityId);
}
void EntityOutlinerWidget::OnTreeItemCollapsed(const QModelIndex& index)
{
m_listModel->OnEntityCollapsed(GetEntityIdFromIndex(index));
AZ::EntityId entityId = GetEntityIdFromIndex(index);
if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId))
{
entityUiHandler->OnOutlinerItemCollapse(index);
}
m_listModel->OnEntityCollapsed(entityId);
}
void EntityOutlinerWidget::OnExpandEntity(const AZ::EntityId& entityId, bool expand)
@@ -1163,7 +1176,7 @@ namespace AzToolsFramework
{
QTimer::singleShot(1, this, [this]() {
m_gui->m_objectTree->setUpdatesEnabled(true);
m_gui->m_objectTree->expandToDepth(0);
m_gui->m_objectTree->expand(m_proxyModel->index(0,0));
});
}
@@ -805,16 +805,15 @@ namespace AzToolsFramework
}
else
{
QString cleanSaveAs(QDir::cleanPath(prefabPath));
AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(prefabPath.toUtf8().constData()).LexicallyNormal();
bool isPathSafeForAssets = false;
for (AZStd::string assetSafeFolder : assetSafeFolders)
for (const AZStd::string& assetSafeFolder : assetSafeFolders)
{
QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str()));
// Compare using clean paths so slash direction does not matter.
// Note that this comparison is case sensitive because some file systems
// Open 3D Engine supports are case sensitive.
if (cleanSaveAs.startsWith(cleanAssetSafeFolder))
AZ::IO::PathView assetSafeFolderView(assetSafeFolder);
// Check if the prefabPath is relative to the safe asset directory.
// The Path classes are being used to make this check case insensitive.
if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView))
{
isPathSafeForAssets = true;
break;
@@ -21,10 +21,16 @@
namespace AzToolsFramework
{
const QColor PrefabUiHandler::m_backgroundColor = QColor("#444444");
const QColor PrefabUiHandler::m_backgroundHoverColor = QColor("#5A5A5A");
const QColor PrefabUiHandler::m_backgroundSelectedColor = QColor("#656565");
const QColor PrefabUiHandler::m_prefabCapsuleColor = QColor("#1E252F");
const QColor PrefabUiHandler::m_prefabCapsuleDisabledColor = QColor("#35383C");
const QColor PrefabUiHandler::m_prefabCapsuleEditColor = QColor("#4A90E2");
const QString PrefabUiHandler::m_prefabIconPath = QString(":/Entity/prefab.svg");
const QString PrefabUiHandler::m_prefabEditIconPath = QString(":/Entity/prefab_edit.svg");
const QString PrefabUiHandler::m_prefabEditOpenIconPath = QString(":/Entity/prefab_edit_open.svg");
const QString PrefabUiHandler::m_prefabEditCloseIconPath = QString(":/Entity/prefab_edit_close.svg");
PrefabUiHandler::PrefabUiHandler()
{
@@ -75,7 +81,7 @@ namespace AzToolsFramework
if (!path.empty())
{
tooltip = QObject::tr("%1").arg(path.Native().data());
tooltip = QObject::tr("Double click to edit.\n%1").arg(path.Native().data());
}
return tooltip;
@@ -102,13 +108,20 @@ namespace AzToolsFramework
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName;
const bool isLastColumn = index.column() == EntityOutlinerListModel::ColumnLockToggle;
const bool hasVisibleChildren = index.data(EntityOutlinerListModel::ExpandedRole).value<bool>() && index.model()->hasChildren(index);
QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName);
const bool hasVisibleChildren =
firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value<bool>() &&
firstColumnIndex.model()->hasChildren(firstColumnIndex);
QColor backgroundColor = m_prefabCapsuleColor;
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
backgroundColor = m_prefabCapsuleEditColor;
}
else if (!(option.state & QStyle::State_Enabled))
{
backgroundColor = m_prefabCapsuleDisabledColor;
}
QPainterPath backgroundPath;
backgroundPath.setFillRule(Qt::WindingFill);
@@ -184,7 +197,8 @@ namespace AzToolsFramework
const bool isFirstColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnName;
const bool isLastColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnLockToggle;
QColor borderColor = m_prefabCapsuleColor;
// There is no legal way of opening prefabs in their default state, so default to disabled.
QColor borderColor = m_prefabCapsuleDisabledColor;
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
borderColor = m_prefabCapsuleEditColor;
@@ -273,6 +287,71 @@ namespace AzToolsFramework
painter->restore();
}
void PrefabUiHandler::PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, [[maybe_unused]] const QModelIndex& index) const
{
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
const QPoint offset = QPoint(-18, 3);
QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName);
const int iconSize = 16;
const bool isHovered = (option.state & QStyle::State_MouseOver);
const bool isSelected = index.data(EntityOutlinerListModel::SelectedRole).template value<bool>();
const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName;
const bool isExpanded =
firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value<bool>() &&
firstColumnIndex.model()->hasChildren(firstColumnIndex);
if (!isFirstColumn || !(option.state & QStyle::State_Enabled))
{
return;
}
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
// Only show the close icon if the prefab is expanded.
// This allows the prefab container to be opened if it was collapsed during propagation.
if (!isExpanded)
{
return;
}
// Use the same color as the background.
QColor backgroundColor = m_backgroundColor;
if (isSelected)
{
backgroundColor = m_backgroundSelectedColor;
}
else if (isHovered)
{
backgroundColor = m_backgroundHoverColor;
}
// Paint a rect to cover up the expander.
QRect rect = QRect(0, 0, 16, 16);
rect.translate(option.rect.topLeft() + offset);
painter->fillRect(rect, backgroundColor);
// Paint the icon.
QIcon closeIcon = QIcon(m_prefabEditCloseIconPath);
painter->drawPixmap(option.rect.topLeft() + offset, closeIcon.pixmap(iconSize));
}
else
{
// Only show the edit icon on hover.
if (!isHovered)
{
return;
}
QIcon openIcon = QIcon(m_prefabEditOpenIconPath);
painter->drawPixmap(option.rect.topLeft() + offset, openIcon.pixmap(iconSize));
}
painter->restore();
}
bool PrefabUiHandler::IsLastVisibleChild(const QModelIndex& parent, const QModelIndex& child)
{
QModelIndex lastVisibleItemIndex = GetLastVisibleChild(parent);
@@ -314,9 +393,53 @@ namespace AzToolsFramework
return Internal_GetLastVisibleChild(model, lastChild);
}
void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const
bool PrefabUiHandler::OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
const QPoint offset = QPoint(-18, 3);
if (m_prefabFocusPublicInterface->IsOwningPrefabInFocusHierarchy(entityId))
{
QRect iconRect = QRect(0, 0, 16, 16);
iconRect.translate(option.rect.topLeft() + offset);
if (iconRect.contains(position))
{
if (!m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
// Focus on this prefab.
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
}
// Don't propagate event.
return true;
}
}
return false;
}
void PrefabUiHandler::OnOutlinerItemCollapse(const QModelIndex& index) const
{
AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value<AZ::u64>());
if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId))
{
auto editorEntityContextId = AzFramework::EntityContextId::CreateNull();
EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId);
// Go one level up.
int length = m_prefabFocusPublicInterface->GetPrefabFocusPathLength(editorEntityContextId);
m_prefabFocusPublicInterface->FocusOnPathIndex(editorEntityContextId, length - 2);
}
}
bool PrefabUiHandler::OnEntityDoubleClick(AZ::EntityId entityId) const
{
// Focus on this prefab
m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId);
// Don't propagate event.
return true;
}
}
@@ -36,7 +36,10 @@ namespace AzToolsFramework
void PaintItemBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void PaintDescendantBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index,
const QModelIndex& descendantIndex) const override;
void OnDoubleClick(AZ::EntityId entityId) const override;
void PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void OnOutlinerItemCollapse(const QModelIndex& index) const override;
bool OnEntityDoubleClick(AZ::EntityId entityId) const override;
private:
Prefab::PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr;
@@ -48,9 +51,15 @@ namespace AzToolsFramework
static constexpr int m_prefabCapsuleRadius = 6;
static constexpr int m_prefabBorderThickness = 2;
static const QColor m_backgroundColor;
static const QColor m_backgroundHoverColor;
static const QColor m_backgroundSelectedColor;
static const QColor m_prefabCapsuleColor;
static const QColor m_prefabCapsuleDisabledColor;
static const QColor m_prefabCapsuleEditColor;
static const QString m_prefabIconPath;
static const QString m_prefabEditIconPath;
static const QString m_prefabEditOpenIconPath;
static const QString m_prefabEditCloseIconPath;
};
} // namespace AzToolsFramework
@@ -20,7 +20,7 @@ namespace AzToolsFramework::ViewportUi::Internal
{
const static int HighlightBorderSize = 5;
const static int TopHighlightBorderSize = 25;
const static char* HighlightBorderColor = "#44B2F8";
const static char* HighlightBorderColor = "#4A90E2";
static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup)
{
@@ -72,7 +72,7 @@ struct FolderRootWatch::PlatformImplementation
// Add the folder to watch and track it
int watchHandle = inotify_add_watch(m_iNotifyHandle,
cleanPath.toUtf8().constData(),
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
{
@@ -95,7 +95,7 @@ struct FolderRootWatch::PlatformImplementation
int watchHandle = inotify_add_watch(m_iNotifyHandle,
dirName.toUtf8().constData(),
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY);
IN_CREATE | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
if (!m_handleToFolderMapLock.tryLock(s_handleToFolderMapLockTimeout))
{
@@ -218,6 +218,10 @@ QTabBar::tab:focus {
color: #666666;
}
#verticalSeparatingLine {
color: #888888;
}
/************** Project Settings **************/
#projectSettings {
margin-top:42px;
@@ -481,6 +485,26 @@ QProgressBar::chunk {
font-weight: 600;
}
#gemCatalogMenuButton {
qproperty-flat: true;
max-width:36px;
min-width:36px;
max-height:24px;
min-height:24px;
}
#GemCatalogCartOverlayGemDownloadHeader {
margin:0;
padding: 0px;
background-color: #333333;
}
#GemCatalogCartOverlayGemDownloadBG {
margin:0;
padding: 0px;
background-color: #444444;
}
#GemCatalogHeaderLabel {
font-size: 12px;
color: #FFFFFF;
@@ -49,6 +49,8 @@ namespace O3DE::ProjectManager
m_stack->addWidget(m_gemCatalogScreen);
vLayout->addWidget(m_stack);
connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest);
// When there are multiple project templates present, we re-gather the gems when changing the selected the project template.
connect(m_newProjectSettingsScreen, &NewProjectSettingsScreen::OnTemplateSelectionChanged, this, [=](int oldIndex, [[maybe_unused]] int newIndex)
{
@@ -133,7 +135,7 @@ namespace O3DE::ProjectManager
}
else
{
emit GotoPreviousScreenRequest();
emit GoToPreviousScreenRequest();
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <DownloadController.h>
#include <DownloadWorker.h>
#include <QMessageBox>
namespace O3DE::ProjectManager
{
DownloadController::DownloadController(QWidget* parent)
: QObject()
, m_lastProgress(0)
, m_parent(parent)
{
m_worker = new DownloadWorker();
m_worker->moveToThread(&m_workerThread);
connect(&m_workerThread, &QThread::started, m_worker, &DownloadWorker::StartDownload);
connect(m_worker, &DownloadWorker::Done, this, &DownloadController::HandleResults);
connect(m_worker, &DownloadWorker::UpdateProgress, this, &DownloadController::UpdateUIProgress);
connect(this, &DownloadController::StartGemDownload, m_worker, &DownloadWorker::StartDownload);
}
DownloadController::~DownloadController()
{
connect(&m_workerThread, &QThread::finished, m_worker, &DownloadController::deleteLater);
m_workerThread.requestInterruption();
m_workerThread.quit();
m_workerThread.wait();
}
void DownloadController::AddGemDownload(const QString& gemName)
{
m_gemNames.push_back(gemName);
if (m_gemNames.size() == 1)
{
m_worker->SetGemToDownload(m_gemNames[0], false);
m_workerThread.start();
}
}
void DownloadController::UpdateUIProgress(int progress)
{
m_lastProgress = progress;
emit GemDownloadProgress(progress);
}
void DownloadController::HandleResults(const QString& result)
{
bool succeeded = true;
if (!result.isEmpty())
{
QMessageBox::critical(nullptr, tr("Gem download"), result);
succeeded = false;
}
m_gemNames.erase(m_gemNames.begin());
emit Done(succeeded);
if (!m_gemNames.empty())
{
emit StartGemDownload(m_gemNames[0]);
}
else
{
m_workerThread.quit();
m_workerThread.wait();
}
}
void DownloadController::HandleCancel()
{
m_workerThread.quit();
emit Done(false);
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,72 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QString>
#include <QThread>
#include <AzCore/std/containers/vector.h>
#endif
QT_FORWARD_DECLARE_CLASS(QProcess)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(DownloadWorker)
class DownloadController : public QObject
{
Q_OBJECT
public:
explicit DownloadController(QWidget* parent = nullptr);
~DownloadController();
void AddGemDownload(const QString& m_gemName);
bool IsDownloadQueueEmpty()
{
return m_gemNames.empty();
}
const AZStd::vector<QString>& GetDownloadQueue() const
{
return m_gemNames;
}
const QString& GetCurrentDownloadingGem() const
{
if (!m_gemNames.empty())
{
return m_gemNames[0];
}
else
{
static const QString emptyString;
return emptyString;
}
}
public slots:
void UpdateUIProgress(int progress);
void HandleResults(const QString& result);
void HandleCancel();
signals:
void StartGemDownload(const QString& gemName);
void Done(bool success = true);
void GemDownloadProgress(int percentage);
private:
DownloadWorker* m_worker;
QThread m_workerThread;
QWidget* m_parent;
AZStd::vector<QString> m_gemNames;
int m_lastProgress;
};
} // namespace O3DE::ProjectManager
@@ -0,0 +1,48 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <DownloadController.h>
#include <DownloadWorker.h>
#include <PythonBindings.h>
namespace O3DE::ProjectManager
{
DownloadWorker::DownloadWorker()
: QObject()
{
}
void DownloadWorker::StartDownload()
{
auto gemDownloadProgress = [=](int downloadProgress)
{
m_downloadProgress = downloadProgress;
emit UpdateProgress(downloadProgress);
};
AZ::Outcome<void, AZStd::string> gemInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress);
if (gemInfoResult.IsSuccess())
{
emit Done("");
}
else
{
emit Done(tr("Gem download failed"));
}
}
void DownloadWorker::SetGemToDownload(const QString& gemName, bool downloadNow)
{
m_gemName = gemName;
if (downloadNow)
{
StartDownload();
}
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,42 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzCore/Outcome/Outcome.h>
#endif
QT_FORWARD_DECLARE_CLASS(QProcess)
namespace O3DE::ProjectManager
{
class DownloadWorker : public QObject
{
// Download was cancelled
inline static const QString DownloadCancelled = QObject::tr("Download Cancelled.");
Q_OBJECT
public:
explicit DownloadWorker();
~DownloadWorker() = default;
public slots:
void StartDownload();
void SetGemToDownload(const QString& gemName, bool downloadNow = true);
signals:
void UpdateProgress(int progress);
void Done(QString result = "");
private:
QString m_gemName;
int m_downloadProgress;
};
} // namespace O3DE::ProjectManager
@@ -29,17 +29,17 @@ namespace O3DE::ProjectManager
topBarFrameWidget->setLayout(topBarHLayout);
QTabWidget* tabWidget = new QTabWidget();
tabWidget->setObjectName("engineTab");
tabWidget->tabBar()->setObjectName("engineTabBar");
tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus);
m_tabWidget = new QTabWidget();
m_tabWidget->setObjectName("engineTab");
m_tabWidget->tabBar()->setObjectName("engineTabBar");
m_tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus);
m_engineSettingsScreen = new EngineSettingsScreen();
m_gemRepoScreen = new GemRepoScreen();
tabWidget->addTab(m_engineSettingsScreen, tr("General"));
tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories"));
topBarHLayout->addWidget(tabWidget);
m_tabWidget->addTab(m_engineSettingsScreen, tr("General"));
m_tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories"));
topBarHLayout->addWidget(m_tabWidget);
vLayout->addWidget(topBarFrameWidget);
@@ -61,4 +61,28 @@ namespace O3DE::ProjectManager
return true;
}
bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen)
{
if (screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum())
{
return true;
}
return false;
}
void EngineScreenCtrl::GoToScreen(ProjectManagerScreen screen)
{
if (screen == m_engineSettingsScreen->GetScreenEnum())
{
m_tabWidget->setCurrentWidget(m_engineSettingsScreen);
m_engineSettingsScreen->NotifyCurrentScreen();
}
else if (screen == m_gemRepoScreen->GetScreenEnum())
{
m_tabWidget->setCurrentWidget(m_gemRepoScreen);
m_gemRepoScreen->NotifyCurrentScreen();
}
}
} // namespace O3DE::ProjectManager
@@ -11,6 +11,8 @@
#include <ScreenWidget.h>
#endif
QT_FORWARD_DECLARE_CLASS(QTabWidget)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(EngineSettingsScreen)
@@ -26,7 +28,10 @@ namespace O3DE::ProjectManager
QString GetTabText() override;
bool IsTab() override;
bool ContainsScreen(ProjectManagerScreen screen) override;
void GoToScreen(ProjectManagerScreen screen) override;
QTabWidget* m_tabWidget = nullptr;
EngineSettingsScreen* m_engineSettingsScreen = nullptr;
GemRepoScreen* m_gemRepoScreen = nullptr;
};
@@ -8,17 +8,21 @@
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <AzCore/std/functional.h>
#include <TagWidget.h>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QLabel>
#include <QPushButton>
#include <TagWidget.h>
#include <QMenu>
#include <QProgressBar>
namespace O3DE::ProjectManager
{
CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, QWidget* parent)
CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
, m_downloadController(downloadController)
{
setObjectName("GemCatalogCart");
@@ -42,6 +46,9 @@ namespace O3DE::ProjectManager
hLayout->addWidget(closeButton);
m_layout->addLayout(hLayout);
// downloading gems
CreateDownloadSection();
// added
CreateGemSection( tr("Gem to be activated"), tr("Gems to be activated"), [=]
{
@@ -149,6 +156,109 @@ namespace O3DE::ProjectManager
update();
}
void CartOverlayWidget::CreateDownloadSection()
{
QWidget* widget = new QWidget();
widget->setFixedWidth(s_width);
m_layout->addWidget(widget);
QVBoxLayout* layout = new QVBoxLayout();
layout->setAlignment(Qt::AlignTop);
widget->setLayout(layout);
QLabel* titleLabel = new QLabel();
titleLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
layout->addWidget(titleLabel);
titleLabel->setText(tr("Gems to be installed"));
// Create header section
QWidget* downloadingGemsWidget = new QWidget();
downloadingGemsWidget->setObjectName("GemCatalogCartOverlayGemDownloadHeader");
layout->addWidget(downloadingGemsWidget);
QVBoxLayout* gemDownloadLayout = new QVBoxLayout();
gemDownloadLayout->setMargin(0);
gemDownloadLayout->setAlignment(Qt::AlignTop);
downloadingGemsWidget->setLayout(gemDownloadLayout);
QLabel* processingQueueLabel = new QLabel("Processing Queue");
gemDownloadLayout->addWidget(processingQueueLabel);
QWidget* downloadingItemWidget = new QWidget();
downloadingItemWidget->setObjectName("GemCatalogCartOverlayGemDownloadBG");
gemDownloadLayout->addWidget(downloadingItemWidget);
QVBoxLayout* downloadingItemLayout = new QVBoxLayout();
downloadingItemLayout->setAlignment(Qt::AlignTop);
downloadingItemWidget->setLayout(downloadingItemLayout);
auto update = [=](int downloadProgress)
{
if (m_downloadController->IsDownloadQueueEmpty())
{
widget->hide();
}
else
{
widget->setUpdatesEnabled(false);
// remove items
QLayoutItem* layoutItem = nullptr;
while ((layoutItem = downloadingItemLayout->takeAt(0)) != nullptr)
{
if (layoutItem->layout())
{
// Gem info row
QLayoutItem* rowLayoutItem = nullptr;
while ((rowLayoutItem = layoutItem->layout()->takeAt(0)) != nullptr)
{
rowLayoutItem->widget()->deleteLater();
}
layoutItem->layout()->deleteLater();
}
if (layoutItem->widget())
{
layoutItem->widget()->deleteLater();
}
}
// Setup gem download rows
const AZStd::vector<QString>& downloadQueue = m_downloadController->GetDownloadQueue();
QLabel* downloadsInProgessLabel = new QLabel("");
downloadsInProgessLabel->setText(
QString("%1 %2").arg(downloadQueue.size()).arg(downloadQueue.size() == 1 ? tr("download in progress...") : tr("downloads in progress...")));
downloadingItemLayout->addWidget(downloadsInProgessLabel);
for (int downloadingGemNumber = 0; downloadingGemNumber < downloadQueue.size(); ++downloadingGemNumber)
{
QHBoxLayout* nameProgressLayout = new QHBoxLayout();
TagWidget* newTag = new TagWidget(downloadQueue[downloadingGemNumber]);
nameProgressLayout->addWidget(newTag);
QLabel* progress = new QLabel(downloadingGemNumber == 0? QString("%1%").arg(downloadProgress) : tr("Queued"));
nameProgressLayout->addWidget(progress);
QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
nameProgressLayout->addSpacerItem(spacer);
QLabel* cancelText = new QLabel(tr("Cancel"));
nameProgressLayout->addWidget(cancelText);
downloadingItemLayout->addLayout(nameProgressLayout);
QProgressBar* downloadProgessBar = new QProgressBar();
downloadingItemLayout->addWidget(downloadProgessBar);
downloadProgessBar->setValue(downloadingGemNumber == 0 ? downloadProgress : 0);
}
widget->setUpdatesEnabled(true);
widget->show();
}
};
auto downloadEnded = [=](bool /*success*/)
{
update(0); // update the list to remove the gem that has finished
};
// connect to download controller data changed
connect(m_downloadController, &DownloadController::GemDownloadProgress, this, update);
connect(m_downloadController, &DownloadController::Done, this, downloadEnded);
update(0);
}
QStringList CartOverlayWidget::ConvertFromModelIndices(const QVector<QModelIndex>& gems) const
{
QStringList gemNames;
@@ -160,9 +270,10 @@ namespace O3DE::ProjectManager
return gemNames;
}
CartButton::CartButton(GemModel* gemModel, QWidget* parent)
CartButton::CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
, m_downloadController(downloadController)
{
m_layout = new QHBoxLayout();
m_layout->setMargin(0);
@@ -239,7 +350,7 @@ namespace O3DE::ProjectManager
delete m_cartOverlay;
}
m_cartOverlay = new CartOverlayWidget(m_gemModel, this);
m_cartOverlay = new CartOverlayWidget(m_gemModel, m_downloadController, this);
connect(m_cartOverlay, &QWidget::destroyed, this, [=]
{
// Reset the overlay pointer on destruction to prevent dangling pointers.
@@ -265,7 +376,7 @@ namespace O3DE::ProjectManager
}
}
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent)
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent)
: QFrame(parent)
{
QHBoxLayout* hLayout = new QHBoxLayout();
@@ -293,8 +404,30 @@ namespace O3DE::ProjectManager
hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding));
hLayout->addSpacerItem(new QSpacerItem(75, 0, QSizePolicy::Fixed));
CartButton* cartButton = new CartButton(gemModel);
CartButton* cartButton = new CartButton(gemModel, downloadController);
hLayout->addWidget(cartButton);
hLayout->addSpacing(16);
// Separating line
QFrame* vLine = new QFrame();
vLine->setFrameShape(QFrame::VLine);
vLine->setObjectName("verticalSeparatingLine");
hLayout->addWidget(vLine);
hLayout->addSpacing(16);
QMenu* gemMenu = new QMenu(this);
m_openGemReposAction = gemMenu->addAction(tr("Show Gem Repos"));
connect(m_openGemReposAction, &QAction::triggered, this,[this](){ emit OpenGemsRepo(); });
QPushButton* gemMenuButton = new QPushButton(this);
gemMenuButton->setObjectName("gemCatalogMenuButton");
gemMenuButton->setMenu(gemMenu);
gemMenuButton->setIcon(QIcon(":/menu.svg"));
gemMenuButton->setIconSize(QSize(36, 24));
hLayout->addWidget(gemMenuButton);
}
void GemCatalogHeaderWidget::ReinitForProject()
@@ -15,12 +15,15 @@
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <TagWidget.h>
#include <DownloadController.h>
#include <QFrame>
#include <QLabel>
#include <QDialog>
#include <QMoveEvent>
#include <QHideEvent>
#include <QVBoxLayout>
#include <QAction>
#endif
namespace O3DE::ProjectManager
@@ -31,16 +34,18 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
CartOverlayWidget(GemModel* gemModel, QWidget* parent = nullptr);
CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
private:
QStringList ConvertFromModelIndices(const QVector<QModelIndex>& gems) const;
using GetTagIndicesCallback = AZStd::function<QVector<QModelIndex>()>;
void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices);
void CreateDownloadSection();
QVBoxLayout* m_layout = nullptr;
GemModel* m_gemModel = nullptr;
DownloadController* m_downloadController = nullptr;
inline constexpr static int s_width = 240;
};
@@ -51,7 +56,7 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
CartButton(GemModel* gemModel, QWidget* parent = nullptr);
CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
~CartButton();
void ShowOverlay();
@@ -64,6 +69,7 @@ namespace O3DE::ProjectManager
QLabel* m_countLabel = nullptr;
QPushButton* m_dropDownButton = nullptr;
CartOverlayWidget* m_cartOverlay = nullptr;
DownloadController* m_downloadController = nullptr;
inline constexpr static int s_iconSize = 24;
inline constexpr static int s_arrowDownIconSize = 8;
@@ -75,13 +81,18 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr);
explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent = nullptr);
~GemCatalogHeaderWidget() = default;
void ReinitForProject();
signals:
void OpenGemsRepo();
private:
AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr;
inline constexpr static int s_height = 60;
QAction* m_openGemReposAction = nullptr;
};
} // namespace O3DE::ProjectManager
@@ -12,6 +12,7 @@
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <GemCatalog/GemRequirementDialog.h>
#include <GemCatalog/GemDependenciesDialog.h>
#include <DownloadController.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
@@ -32,9 +33,13 @@ namespace O3DE::ProjectManager
vLayout->setSpacing(0);
setLayout(vLayout);
m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel);
m_downloadController = new DownloadController();
m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel, m_downloadController);
vLayout->addWidget(m_headerWidget);
connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setMargin(0);
vLayout->addLayout(hLayout);
@@ -191,6 +196,27 @@ namespace O3DE::ProjectManager
return EnableDisableGemsResult::Success;
}
void GemCatalogScreen::HandleOpenGemRepo()
{
QVector<QModelIndex> gemsToBeAdded = m_gemModel->GatherGemsToBeAdded(true);
QVector<QModelIndex> gemsToBeRemoved = m_gemModel->GatherGemsToBeRemoved(true);
if (!gemsToBeAdded.empty() || !gemsToBeRemoved.empty())
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(
nullptr, "Pending Changes",
"There are some unsaved changes to the gem selection,<br> they will be lost if you change screens.<br> Are you sure?",
QMessageBox::No | QMessageBox::Yes);
if (warningResult != QMessageBox::Yes)
{
return;
}
}
emit ChangeScreenRequest(ProjectManagerScreen::GemRepos);
}
ProjectManagerScreen GemCatalogScreen::GetScreenEnum()
{
return ProjectManagerScreen::GemCatalog;
@@ -39,8 +39,13 @@ namespace O3DE::ProjectManager
EnableDisableGemsResult EnableDisableGemsForProject(const QString& projectPath);
GemModel* GetGemModel() const { return m_gemModel; }
DownloadController* GetDownloadController() const { return m_downloadController; }
private slots:
void HandleOpenGemRepo();
private:
void FillModel(const QString& projectPath);
GemListView* m_gemListView = nullptr;
@@ -50,5 +55,6 @@ namespace O3DE::ProjectManager
GemSortFilterProxyModel* m_proxModel = nullptr;
QVBoxLayout* m_filterWidgetLayout = nullptr;
GemFilterWidget* m_filterWidget = nullptr;
DownloadController* m_downloadController = nullptr;
};
} // namespace O3DE::ProjectManager
@@ -301,6 +301,7 @@ namespace O3DE::ProjectManager
m_enableGemProject = pybind11::module::import("o3de.enable_gem");
m_disableGemProject = pybind11::module::import("o3de.disable_gem");
m_editProjectProperties = pybind11::module::import("o3de.project_properties");
m_download = pybind11::module::import("o3de.download");
m_repo = pybind11::module::import("o3de.repo");
m_pathlib = pybind11::module::import("pathlib");
@@ -1116,4 +1117,30 @@ namespace O3DE::ProjectManager
std::sort(gemRepos.begin(), gemRepos.end());
return AZ::Success(AZStd::move(gemRepos));
}
AZ::Outcome<void, AZStd::string> PythonBindings::DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback)
{
bool downloadSucceeded = false;
auto result = ExecuteWithLockErrorHandling(
[&]
{
auto downloadResult = m_download.attr("download_gem")(
QString_To_Py_String(gemName), // gem name
pybind11::none(), // destination path
false// skip auto register
);
downloadSucceeded = (downloadResult.cast<int>() == 0);
});
if (!result.IsSuccess())
{
return result;
}
else if (!downloadSucceeded)
{
return AZ::Failure<AZStd::string>("Failed to download gem.");
}
return AZ::Success();
}
}
@@ -63,6 +63,7 @@ namespace O3DE::ProjectManager
bool AddGemRepo(const QString& repoUri) override;
bool RemoveGemRepo(const QString& repoUri) override;
AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) override;
private:
AZ_DISABLE_COPY_MOVE(PythonBindings);
@@ -89,6 +90,7 @@ namespace O3DE::ProjectManager
pybind11::handle m_enableGemProject;
pybind11::handle m_disableGemProject;
pybind11::handle m_editProjectProperties;
pybind11::handle m_download;
pybind11::handle m_repo;
pybind11::handle m_pathlib;
};
@@ -200,6 +200,8 @@ namespace O3DE::ProjectManager
* @return A list of gem repo infos.
*/
virtual AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() = 0;
virtual AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) = 0;
};
using PythonBindingsInterface = AZ::Interface<IPythonBindings>;
@@ -47,6 +47,14 @@ namespace O3DE::ProjectManager
return tr("Missing");
}
virtual bool ContainsScreen([[maybe_unused]] ProjectManagerScreen screen)
{
return false;
}
virtual void GoToScreen([[maybe_unused]] ProjectManagerScreen screen)
{
}
//! Notify this screen it is the current screen
virtual void NotifyCurrentScreen()
{
@@ -55,7 +63,7 @@ namespace O3DE::ProjectManager
signals:
void ChangeScreenRequest(ProjectManagerScreen screen);
void GotoPreviousScreenRequest();
void GoToPreviousScreenRequest();
void ResetScreenRequest(ProjectManagerScreen screen);
void NotifyCurrentProject(const QString& projectPath);
void NotifyBuildProject(const ProjectInfo& projectInfo);
@@ -83,11 +83,28 @@ namespace O3DE::ProjectManager
bool ScreensCtrl::ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit)
{
ScreenWidget* newScreen = nullptr;
const auto iterator = m_screenMap.find(screen);
if (iterator != m_screenMap.end())
{
newScreen = iterator.value();
}
else
{
// Check if screen is contained by another screen
for (ScreenWidget* checkingScreen : m_screenMap)
{
if (checkingScreen->ContainsScreen(screen))
{
newScreen = checkingScreen;
break;
}
}
}
if (newScreen)
{
ScreenWidget* currentScreen = GetCurrentScreen();
ScreenWidget* newScreen = iterator.value();
if (currentScreen != newScreen)
{
@@ -109,6 +126,11 @@ namespace O3DE::ProjectManager
newScreen->NotifyCurrentScreen();
if (iterator == m_screenMap.end())
{
newScreen->GoToScreen(screen);
}
return true;
}
}
@@ -116,7 +138,7 @@ namespace O3DE::ProjectManager
return false;
}
bool ScreensCtrl::GotoPreviousScreen()
bool ScreensCtrl::GoToPreviousScreen()
{
if (!m_screenVisitOrder.isEmpty())
{
@@ -171,7 +193,7 @@ namespace O3DE::ProjectManager
m_screenMap.insert(screen, newScreen);
connect(newScreen, &ScreenWidget::ChangeScreenRequest, this, &ScreensCtrl::ChangeToScreen);
connect(newScreen, &ScreenWidget::GotoPreviousScreenRequest, this, &ScreensCtrl::GotoPreviousScreen);
connect(newScreen, &ScreenWidget::GoToPreviousScreenRequest, this, &ScreensCtrl::GoToPreviousScreen);
connect(newScreen, &ScreenWidget::ResetScreenRequest, this, &ScreensCtrl::ResetScreen);
connect(newScreen, &ScreenWidget::NotifyCurrentProject, this, &ScreensCtrl::NotifyCurrentProject);
connect(newScreen, &ScreenWidget::NotifyBuildProject, this, &ScreensCtrl::NotifyBuildProject);
@@ -41,7 +41,7 @@ namespace O3DE::ProjectManager
public slots:
bool ChangeToScreen(ProjectManagerScreen screen);
bool ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit = true);
bool GotoPreviousScreen();
bool GoToPreviousScreen();
void ResetScreen(ProjectManagerScreen screen);
void ResetAllScreens();
void DeleteScreen(ProjectManagerScreen screen);
@@ -40,6 +40,10 @@ namespace O3DE::ProjectManager
m_updateSettingsScreen = new UpdateProjectSettingsScreen();
m_gemCatalogScreen = new GemCatalogScreen();
connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, [this](ProjectManagerScreen screen){
emit ChangeScreenRequest(screen);
});
m_stack = new QStackedWidget(this);
m_stack->setObjectName("body");
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding));
@@ -118,7 +122,7 @@ namespace O3DE::ProjectManager
{
if (UpdateProjectSettings(true))
{
emit GotoPreviousScreenRequest();
emit GoToPreviousScreenRequest();
}
}
}
@@ -136,6 +140,11 @@ namespace O3DE::ProjectManager
}
else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
{
if (!m_gemCatalogScreen->GetDownloadController()->IsDownloadQueueEmpty())
{
QMessageBox::critical(this, tr("Gems downloading"), tr("You must wait for gems to finish downloading before continuing."));
return;
}
// Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
const GemCatalogScreen::EnableDisableGemsResult result = m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path);
if (result == GemCatalogScreen::EnableDisableGemsResult::Failed)
@@ -29,6 +29,10 @@ set(FILES
Source/FormImageBrowseEditWidget.cpp
Source/GemsSubWidget.h
Source/GemsSubWidget.cpp
Source/DownloadController.h
Source/DownloadController.cpp
Source/DownloadWorker.h
Source/DownloadWorker.cpp
Source/PathValidator.h
Source/PathValidator.cpp
Source/ProjectManagerWindow.h
@@ -404,18 +404,18 @@ namespace AZ
bool AzslCompiler::ParseSrgPopulateRootConstantData(const rapidjson::Document& input, RootConstantData& rootConstantData) const
{
if (input.HasMember("InlineConstantBuffer"))
if (input.HasMember("RootConstantBuffer"))
{
const rapidjson::Value& rootConstantBufferValue = input["InlineConstantBuffer"];
AZ_Assert(rootConstantBufferValue.IsObject(), "InlineConstantBuffer is not an object");
const rapidjson::Value& rootConstantBufferValue = input["RootConstantBuffer"];
AZ_Assert(rootConstantBufferValue.IsObject(), "RootConstantBuffer is not an object");
for (rapidjson::Value::ConstMemberIterator itr = rootConstantBufferValue.MemberBegin(); itr != rootConstantBufferValue.MemberEnd(); ++itr)
{
AZStd::string_view rootConstantBufferMemberName = itr->name.GetString();
const rapidjson::Value& rootConstantBufferMemberValue = itr->value;
if (rootConstantBufferMemberName == "bufferForInlineConstants")
if (rootConstantBufferMemberName == "bufferForRootConstants")
{
AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForInlineConstants is not an object");
AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForRootConstants is not an object");
for (rapidjson::Value::ConstMemberIterator itr2 = rootConstantBufferMemberValue.MemberBegin(); itr2 != rootConstantBufferMemberValue.MemberEnd(); ++itr2)
{
@@ -442,14 +442,14 @@ namespace AZ
}
}
}
else if (rootConstantBufferMemberName == "inputsForInlineConstants")
else if (rootConstantBufferMemberName == "inputsForRootConstants")
{
AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForInlineConstants is not an array");
AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForRootConstants is not an array");
for (rapidjson::Value::ConstValueIterator itr2 = rootConstantBufferMemberValue.Begin(); itr2 != rootConstantBufferMemberValue.End(); ++itr2)
{
const rapidjson::Value& rootConstantBufferValue2 = *itr2;
AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForInlineConstants is not an object");
AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForRootConstants is not an object");
SrgConstantData rootConstantInputs;
@@ -81,7 +81,7 @@ namespace AZ
// Register Shader Asset Builder
AssetBuilderSDK::AssetBuilderDesc shaderAssetBuilderDescriptor;
shaderAssetBuilderDescriptor.m_name = "Shader Asset Builder";
shaderAssetBuilderDescriptor.m_version = 104; // ATOM-15871
shaderAssetBuilderDescriptor.m_version = 105; // [AZSL] Changing inlineConstant to rootConstant keyword work.
// .shader file changes trigger rebuilds
shaderAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern( AZStd::string::format("*.%s", RPI::ShaderSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
shaderAssetBuilderDescriptor.m_busId = azrtti_typeid<ShaderAssetBuilder>();
@@ -96,7 +96,7 @@ namespace AZ
shaderVariantAssetBuilderDescriptor.m_name = "Shader Variant Asset Builder";
// Both "Shader Variant Asset Builder" and "Shader Asset Builder" produce ShaderVariantAsset products. If you update
// ShaderVariantAsset you will need to update BOTH version numbers, not just "Shader Variant Asset Builder".
shaderVariantAssetBuilderDescriptor.m_version = 25; // ATOM-15871
shaderVariantAssetBuilderDescriptor.m_version = 26; // [AZSL] Changing inlineConstant to rootConstant keyword work.
shaderVariantAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", RPI::ShaderVariantListSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard));
shaderVariantAssetBuilderDescriptor.m_busId = azrtti_typeid<ShaderVariantAssetBuilder>();
shaderVariantAssetBuilderDescriptor.m_createJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::CreateJobs, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2);
@@ -183,7 +183,7 @@ namespace AZ
// access the root constants reflection
if (!azslc.ParseSrgPopulateRootConstantData(
outcomes[AzslSubProducts::srg].GetValue(),
rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section)
rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section)
{
AZ_Error(builderName, false, "Failed to obtain root constant data reflection");
return AssetBuilderSDK::ProcessJobResult_Failed;
@@ -561,7 +561,7 @@ namespace AZ
// Access the root constants reflection
if (!azslCompiler.ParseSrgPopulateRootConstantData(
jsonOutcome.GetValue(),
rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section)
rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section)
{
AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to obtain root constant data reflection");
return false;
@@ -1,7 +1,7 @@
{
"description": "Base material for the reflection probe visualization model.",
"version": 1,
"propertyLayout": {
"version": 1,
"properties": {
"general": [
{
@@ -1,7 +1,7 @@
{
"description": "Base material for the reflection probe visualization model.",
"version": 1,
"propertyLayout": {
"version": 1,
"properties": {
"settings": [
{
@@ -1,7 +1,7 @@
{
"description": "Material Type with properties used to define Enhanced PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model, with advanced features like subsurface scattering, transmission, and anisotropy.",
"version": 3,
"propertyLayout": {
"version": 3,
"groups": [
{
"name": "baseColor",
@@ -1,7 +1,7 @@
{
"description": "Material Type tailored for rendering skin, with support for blended wrinkle maps that work with animated vertex blend shapes.",
"version": 3,
"propertyLayout": {
"version": 3,
"groups": [
{
"name": "baseColor",
@@ -1,7 +1,7 @@
{
"description": "Similar to StandardPBR but supports multiple layers blended together.",
"version": 3,
"propertyLayout": {
"version": 3,
"groups": [
{
"name": "blend",
@@ -48,6 +48,14 @@
}
]
},
"Supervariants":
[
{
"Name": "",
"PlusArguments": "--no-alignment-validation"
}
],
"DrawList" : "forward"
}

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