Răsfoiți Sursa

Merge branch 'stabilization/2110' of https://github.com/aws-lumberyard/o3de into Prism/CLIDisplayLastError

Signed-off-by: AMZN-Phil <[email protected]>
AMZN-Phil 3 ani în urmă
părinte
comite
7787ceb04c
65 a modificat fișierele cu 801 adăugiri și 386 ștergeri
  1. 1 2
      AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges.py
  2. 164 0
      AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/Terrain_SupportsPhysics.py
  3. 3 1
      AutomatedTesting/Gem/PythonTests/Terrain/TestSuite_Main.py
  4. 10 1
      AutomatedTesting/Levels/Base/Base.prefab
  5. 1 1
      Code/Editor/MainWindow.cpp
  6. 6 4
      Code/Editor/Settings.cpp
  7. 1 1
      Code/Editor/Settings.h
  8. 5 1
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/ComponentModeCollection.cpp
  9. 5 2
      Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/EditorBaseComponentMode.cpp
  10. 15 2
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp
  11. 41 2
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp
  12. 6 2
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.h
  13. 3 2
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiManager.cpp
  14. 2 1
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiManager.h
  15. 6 3
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiRequestBus.h
  16. 0 1
      Code/Tools/AssetProcessor/native/utilities/assetUtils.h
  17. 1 0
      Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp
  18. 1 6
      Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp
  19. 91 43
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp
  20. 18 7
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h
  21. 37 4
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp
  22. 21 7
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h
  23. 9 5
      Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp
  24. 1 0
      Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h
  25. 1 1
      Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.cpp
  26. 1 0
      Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp
  27. 1 0
      Code/Tools/ProjectManager/Source/ProjectButtonWidget.h
  28. 16 3
      Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp
  29. 9 0
      Code/Tools/ProjectManager/Source/ProjectsScreen.cpp
  30. 1 0
      Code/Tools/ProjectManager/Source/ProjectsScreen.h
  31. 2 2
      Code/Tools/ProjectManager/Source/PythonBindings.cpp
  32. 2 2
      Code/Tools/ProjectManager/Source/PythonBindings.h
  33. 3 3
      Code/Tools/ProjectManager/Source/PythonBindingsInterface.h
  34. 2 3
      Code/Tools/ProjectManager/Source/ScreenWidget.h
  35. 22 1
      Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp
  36. 4 2
      Code/Tools/ProjectManager/Source/UpdateProjectCtrl.h
  37. 1 1
      Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp
  38. 15 11
      Gems/Atom/Asset/ImageProcessingAtom/Assets/Config/ImageBuilder.settings
  39. 5 3
      Gems/Atom/Asset/ImageProcessingAtom/Assets/Config/Reflectance.preset
  40. 0 1
      Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ModelPreset.h
  41. 0 1
      Gems/Atom/Feature/Common/Code/Source/Utils/EditorModelPreset.cpp
  42. 1 3
      Gems/Atom/Feature/Common/Code/Source/Utils/ModelPreset.cpp
  43. 3 0
      Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt
  44. 16 2
      Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Util/Util.h
  45. 34 1
      Gems/Atom/Tools/AtomToolsFramework/Code/Source/Util/Util.cpp
  46. 0 2
      Gems/Atom/Tools/MaterialEditor/Code/CMakeLists.txt
  47. 0 18
      Gems/Atom/Tools/MaterialEditor/Code/Include/Atom/Viewport/MaterialViewportRequestBus.h
  48. 10 91
      Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/MaterialViewportComponent.cpp
  49. 0 10
      Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/MaterialViewportComponent.h
  50. 8 4
      Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/LightingPresetBrowserDialog.cpp
  51. 4 4
      Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/ModelPresetBrowserDialog.cpp
  52. 44 23
      Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/PresetBrowserDialog.cpp
  53. 3 3
      Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/PresetBrowserDialog.h
  54. 3 3
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewContent.cpp
  55. 1 1
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewContent.h
  56. 42 47
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewUtils.cpp
  57. 16 12
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewUtils.h
  58. 9 12
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnail.cpp
  59. 1 2
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnail.h
  60. 56 6
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnailRenderer.cpp
  61. 9 3
      Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnailRenderer.h
  62. 5 2
      Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/NodePalette/TreeItems/NodePaletteTreeItem.cpp
  63. 0 5
      Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/NodePalette/TreeItems/NodePaletteTreeItem.h
  64. 1 0
      Gems/ScriptCanvas/Code/Editor/View/Widgets/NodePalette/NodePaletteModel.cpp
  65. 2 0
      Gems/ScriptCanvas/Code/Editor/View/Windows/MainWindow.cpp

+ 1 - 2
AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges.py

@@ -12,13 +12,12 @@ class Tests():
     add_terrain_collider                    = ("Terrain Physics Heightfield Collider component added", "Failed to add a Terrain Physics Heightfield Collider component")
     box_dimensions_changed                  = ("Aabb dimensions changed successfully", "Failed change Aabb dimensions")
     configuration_changed                   = ("Terrain size changed successfully", "Failed terrain size change")
-    no_errors_and_warnings_found            = ("No errors and warnings found",   "Found errors and warnings")
 #fmt: on
 
 def TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges():
     """
     Summary:
-    Test aspects of the TerrainHeightGradientList through the BehaviorContext and the Property Tree.
+    Test aspects of the Terrain Physics Heightfield Collider through the BehaviorContext and the Property Tree.
 
     Test Steps:
     Expected Behavior:

+ 164 - 0
AutomatedTesting/Gem/PythonTests/Terrain/EditorScripts/Terrain_SupportsPhysics.py

@@ -0,0 +1,164 @@
+"""
+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
+"""
+
+#fmt: off
+class Tests():
+    create_terrain_spawner_entity               = ("Terrain_spawner_entity created successfully", "Failed to create terrain_spawner_entity")
+    create_height_provider_entity               = ("Height_provider_entity created successfully", "Failed to create height_provider_entity")
+    create_test_ball                            = ("Ball created successfully",    "Failed to create Ball")
+    box_dimensions_changed                      = ("Aabb dimensions changed successfully", "Failed change Aabb dimensions")
+    shape_changed                               = ("Shape changed successfully", "Failed Shape change")
+    entity_added                                = ("Entity added successfully", "Failed Entity add")
+    frequency_changed                           = ("Frequency changed successfully", "Failed Frequency change")
+    shape_set                                   = ("Shape set to Sphere successfully", "Failed to set Sphere shape")
+    test_collision                              = ("Ball collided with terrain", "Ball failed to collide with terrain")
+    no_errors_and_warnings_found                = ("No errors and warnings found", "Found errors and warnings")
+#fmt: on
+
+def Terrain_SupportsPhysics():
+    """
+    Summary:
+    Test aspects of the TerrainHeightGradientList through the BehaviorContext and the Property Tree.
+
+    Test Steps:
+    Expected Behavior:
+    The Editor is stable there are no warnings or errors.
+
+    Test Steps:
+     1) Load the base level
+     2) Create 2 test entities, one parent at 512.0, 512.0, 50.0 and one child at the default position and add the required components
+     2a) Create a ball at 600.0, 600.0, 46.0 - This position is bot too high over the heighfield so will collide in a reasonable time
+     3) Start the Tracer to catch any errors and warnings
+     4) Change the Axis Aligned Box Shape dimensions
+     5) Set the Vegetation Shape reference to TestEntity1
+     6) Set the FastNoise gradient frequency to 0.01
+     7) Set the Gradient List to TestEntity2
+     8) Set the PhysX Collider to Sphere mode
+     9) Disable and Enable the Terrain Gradient List so that it is recognised
+     10) Enter game mode and test if the ball hits the heightfield within 3 seconds
+     11) Verify there are no errors and warnings in the logs
+
+
+    :return: None
+    """
+
+    from editor_python_test_tools.editor_entity_utils import EditorEntity
+    from editor_python_test_tools.utils import TestHelper as helper, Report
+    from editor_python_test_tools.utils import Report, Tracer
+    import editor_python_test_tools.hydra_editor_utils as hydra
+    import azlmbr.math as azmath
+    import azlmbr.legacy.general as general
+    import azlmbr.bus as bus
+    import azlmbr.editor as editor
+    import math
+
+    SET_BOX_X_SIZE = 1024.0
+    SET_BOX_Y_SIZE = 1024.0
+    SET_BOX_Z_SIZE = 100.0
+    
+    helper.init_idle()
+    
+    # 1) Load the level
+    helper.open_level("", "Base")
+    helper.wait_for_condition(lambda: general.get_current_level_name() == "Base", 2.0)
+
+    #1a) Load the level components
+    hydra.add_level_component("Terrain World")
+    hydra.add_level_component("Terrain World Renderer")
+
+    # 2) Create 2 test entities, one parent at 512.0, 512.0, 50.0 and one child at the default position and add the required components
+    entity1_components_to_add = ["Axis Aligned Box Shape", "Terrain Layer Spawner", "Terrain Height Gradient List", "Terrain Physics Heightfield Collider", "PhysX Heightfield Collider"]
+    entity2_components_to_add = ["Vegetation Reference Shape", "Gradient Transform Modifier", "FastNoise Gradient"]
+    ball_components_to_add = ["Sphere Shape", "PhysX Collider", "PhysX Rigid Body"]
+    terrain_spawner_entity = hydra.Entity("TestEntity1")
+    terrain_spawner_entity.create_entity(azmath.Vector3(512.0, 512.0, 50.0), entity1_components_to_add)
+    Report.result(Tests.create_terrain_spawner_entity, terrain_spawner_entity.id.IsValid())
+    height_provider_entity = hydra.Entity("TestEntity2")
+    height_provider_entity.create_entity(azmath.Vector3(0.0, 0.0, 0.0), entity2_components_to_add,terrain_spawner_entity.id)
+    Report.result(Tests.create_height_provider_entity, height_provider_entity.id.IsValid())
+    # 2a) Create a ball at 600.0, 600.0, 46.0 - This position is bot too high over the heighfield so will collide in a reasonable time
+    ball = hydra.Entity("Ball")
+    ball.create_entity(azmath.Vector3(600.0, 600.0, 46.0), ball_components_to_add)
+    Report.result(Tests.create_test_ball, ball.id.IsValid())
+    # Give everything a chance to finish initializing.
+    general.idle_wait_frames(1)
+
+    # 3) Start the Tracer to catch any errors and warnings
+    with Tracer() as section_tracer:
+        # 4) Change the Axis Aligned Box Shape dimensions
+        box_dimensions = azmath.Vector3(SET_BOX_X_SIZE, SET_BOX_Y_SIZE, SET_BOX_Z_SIZE)
+        terrain_spawner_entity.get_set_test(0, "Axis Aligned Box Shape|Box Configuration|Dimensions", box_dimensions)
+        box_shape_dimensions = hydra.get_component_property_value(terrain_spawner_entity.components[0], "Axis Aligned Box Shape|Box Configuration|Dimensions")
+        Report.result(Tests.box_dimensions_changed, box_dimensions == box_shape_dimensions)
+        
+        # 5) Set the Vegetaion Shape reference to TestEntity1
+        height_provider_entity.get_set_test(0, "Configuration|Shape Entity Id", terrain_spawner_entity.id)
+        entityId = hydra.get_component_property_value(height_provider_entity.components[0], "Configuration|Shape Entity Id")
+        Report.result(Tests.shape_changed, entityId == terrain_spawner_entity.id)
+
+        # 6) Set the FastNoise Gradient frequency to 0.01
+        Frequency = 0.01
+        height_provider_entity.get_set_test(2, "Configuration|Frequency", Frequency)
+        FrequencyVal = hydra.get_component_property_value(height_provider_entity.components[2], "Configuration|Frequency")
+        Report.result(Tests.frequency_changed, math.isclose(Frequency, FrequencyVal, abs_tol = 0.00001))
+
+        # 7) Set the Gradient List to TestEntity2
+        pte = hydra.get_property_tree(terrain_spawner_entity.components[2])
+        pte.add_container_item("Configuration|Gradient Entities", 0, height_provider_entity.id)
+        checkID = pte.get_container_item("Configuration|Gradient Entities", 0)
+        Report.result(Tests.entity_added, checkID.GetValue() == height_provider_entity.id)
+
+        # 8) Set the PhysX Collider to Sphere mode
+        shape = 0
+        hydra.get_set_test(ball, 1, "Shape Configuration|Shape", shape)
+        setShape = hydra.get_component_property_value(ball.components[1], "Shape Configuration|Shape")
+        Report.result(Tests.shape_set, shape == setShape)
+
+        # 9) Disable and Enable the Terrain Gradient List so that it is recognised
+        editor.EditorComponentAPIBus(bus.Broadcast, 'EnableComponents', [terrain_spawner_entity.components[2]])
+
+        general.enter_game_mode()
+
+        general.idle_wait_frames(1)
+
+        # 10) Enter game mode and test if the ball hits the heightfield within 3 seconds
+        TIMEOUT = 3.0
+
+        class Collider:
+            id = general.find_game_entity("Ball")
+            touched_ground = False
+
+        terrain_id = general.find_game_entity("TestEntity1")
+ 
+        def on_collision_begin(args):
+            other_id = args[0]
+            if other_id.Equal(terrain_id):
+                Report.info("Touched ground")
+                Collider.touched_ground = True
+
+        handler = azlmbr.physics.CollisionNotificationBusHandler()
+        handler.connect(Collider.id)
+        handler.add_callback("OnCollisionBegin", on_collision_begin)
+
+        helper.wait_for_condition(lambda: Collider.touched_ground, TIMEOUT)
+        Report.result(Tests.test_collision, Collider.touched_ground)
+
+        general.exit_game_mode()
+
+    # 11) Verify there are no errors and warnings in the logs
+    helper.wait_for_condition(lambda: section_tracer.has_errors or section_tracer.has_asserts, 1.0)
+    for error_info in section_tracer.errors:
+        Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
+    for assert_info in section_tracer.asserts:
+        Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
+    
+
+if __name__ == "__main__":
+
+    from editor_python_test_tools.utils import Report
+    Report.start_test(Terrain_SupportsPhysics)
+

+ 3 - 1
AutomatedTesting/Gem/PythonTests/Terrain/TestSuite_Main.py

@@ -19,7 +19,9 @@ from ly_test_tools.o3de.editor_test import EditorTestSuite, EditorSingleTest
 @pytest.mark.parametrize("launcher_platform", ['windows_editor'])
 @pytest.mark.parametrize("project", ["AutomatedTesting"])
 class TestAutomation(EditorTestSuite):
-    #global_extra_cmdline_args=["--regset=/Amazon/Preferences/EnablePrefabSystem=true"]
 
     class test_AxisAlignedBoxShape_ConfigurationWorks(EditorSingleTest):
         from .EditorScripts import TerrainPhysicsCollider_ChangesSizeWithAxisAlignedBoxShapeChanges as test_module
+
+    class test_Terrain_SupportsPhysics(EditorSingleTest):
+        from .EditorScripts import Terrain_SupportsPhysics as test_module

+ 10 - 1
AutomatedTesting/Levels/Base/Base.prefab

@@ -17,7 +17,16 @@
             },
             "Component_[14126657869720434043]": {
                 "$type": "EditorEntitySortComponent",
-                "Id": 14126657869720434043
+                "Id": 14126657869720434043,
+                "ChildEntityOrderEntryArray": [
+                    {
+                        "EntityId": ""
+                    },
+                    {
+                        "EntityId": "",
+                        "SortIndex": 1
+                    }
+                ]
             },
             "Component_[15230859088967841193]": {
                 "$type": "{27F1E1A1-8D9D-4C3B-BD3A-AFB9762449C0} TransformComponent",

+ 1 - 1
Code/Editor/MainWindow.cpp

@@ -519,7 +519,7 @@ MainWindow* MainWindow::instance()
 
 void MainWindow::closeEvent(QCloseEvent* event)
 {
-    gSettings.Save();
+    gSettings.Save(true);
 
     AzFramework::SystemCursorState currentCursorState;
     bool isInGameMode = false;

+ 6 - 4
Code/Editor/Settings.cpp

@@ -473,7 +473,7 @@ void SEditorSettings::LoadValue(const char* sSection, const char* sKey, ESystemC
 }
 
 //////////////////////////////////////////////////////////////////////////
-void SEditorSettings::Save()
+void SEditorSettings::Save(bool isEditorClosing)
 {
     QString strStringPlaceholder;
 
@@ -640,14 +640,16 @@ void SEditorSettings::Save()
     // --- Settings Registry values
 
     // Prefab System UI
-    AzFramework::ApplicationRequests::Bus::Broadcast(
-        &AzFramework::ApplicationRequests::SetPrefabSystemEnabled, prefabSystem);
+    AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::SetPrefabSystemEnabled, prefabSystem);
 
     AzToolsFramework::Prefab::PrefabLoaderInterface* prefabLoaderInterface =
         AZ::Interface<AzToolsFramework::Prefab::PrefabLoaderInterface>::Get();
     prefabLoaderInterface->SetSaveAllPrefabsPreference(levelSaveSettings.saveAllPrefabsPreference);
 
-    SaveSettingsRegistryFile();
+    if (!isEditorClosing)
+    {
+        SaveSettingsRegistryFile();
+    }
 }
 
 //////////////////////////////////////////////////////////////////////////

+ 1 - 1
Code/Editor/Settings.h

@@ -267,7 +267,7 @@ struct SANDBOX_API SEditorSettings
 AZ_POP_DISABLE_DLL_EXPORT_BASECLASS_WARNING
     SEditorSettings();
     ~SEditorSettings() = default;
-    void    Save();
+    void    Save(bool isEditorClosing = false);
     void    Load();
     void    LoadCloudSettings();
 

+ 5 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/ComponentModeCollection.cpp

@@ -448,7 +448,11 @@ namespace AzToolsFramework
 
                     ViewportUi::ViewportUiRequestBus::Event(
                         ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder,
-                        componentMode.m_componentMode->GetComponentModeName().c_str());
+                        componentMode.m_componentMode->GetComponentModeName().c_str(),
+                        []
+                        {
+                            ComponentModeSystemRequestBus::Broadcast(&ComponentModeSystemRequests::EndComponentMode);
+                        });
                 }
 
                 RefreshActions();

+ 5 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/ComponentMode/EditorBaseComponentMode.cpp

@@ -55,8 +55,11 @@ namespace AzToolsFramework
                 GetEntityComponentIdPair(), elementIdsToDisplay);
             // create the component mode border with the specific name for this component mode
             ViewportUi::ViewportUiRequestBus::Event(
-                ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder,
-                GetComponentModeName());
+                ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, GetComponentModeName(),
+                []
+                {
+                    ComponentModeSystemRequestBus::Broadcast(&ComponentModeSystemRequests::EndComponentMode);
+                });
             // set the EntityComponentId for this ComponentMode to active in the ComponentModeViewportUi system
             ComponentModeViewportUiRequestBus::Event(
                 GetComponentType(), &ComponentModeViewportUiRequestBus::Events::SetViewportUiActiveEntityComponentId,

+ 15 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp

@@ -27,6 +27,7 @@
 #include <AzToolsFramework/Manipulators/ScaleManipulators.h>
 #include <AzToolsFramework/Manipulators/TranslationManipulators.h>
 #include <AzToolsFramework/Maths/TransformUtils.h>
+#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
 #include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
 #include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
 #include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
@@ -1014,6 +1015,15 @@ namespace AzToolsFramework
         ToolsApplicationNotificationBus::Broadcast(&ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplay, Refresh_Values);
     }
 
+    // leaves focus mode by focusing on the parent of the current perfab in the entity outliner
+    static void LeaveFocusMode()
+    {
+        if (auto prefabFocusPublicInterface = AZ::Interface<Prefab::PrefabFocusPublicInterface>::Get())
+        {
+            prefabFocusPublicInterface->FocusOnParentOfFocusedPrefab(GetEntityContextId());
+        }
+    }
+
     EditorTransformComponentSelection::EditorTransformComponentSelection(const EditorVisibleEntityDataCache* entityDataCache)
         : m_entityDataCache(entityDataCache)
     {
@@ -3674,7 +3684,8 @@ namespace AzToolsFramework
         case ViewportEditorMode::Focus:
             {
                 ViewportUi::ViewportUiRequestBus::Event(
-                    ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode");
+                    ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode",
+                    LeaveFocusMode);
             }
             break;
         case ViewportEditorMode::Default:
@@ -3703,12 +3714,14 @@ namespace AzToolsFramework
                 if (editorModeState.IsModeActive(ViewportEditorMode::Focus))
                 {
                     ViewportUi::ViewportUiRequestBus::Event(
-                        ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode");
+                        ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode",
+                        LeaveFocusMode);
                 }
             }
             break;
         case ViewportEditorMode::Focus:
             {
+                
                 ViewportUi::ViewportUiRequestBus::Event(
                     ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RemoveViewportBorder);
             }

+ 41 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp

@@ -21,6 +21,9 @@ namespace AzToolsFramework::ViewportUi::Internal
 {
     const static int HighlightBorderSize = 5;
     const static char* HighlightBorderColor = "#4A90E2";
+    const static int HighlightBorderBackButtonMargin = 5;
+    const static int HighlightBorderBackButtonIconSize = 20;
+    const static char* HighlightBorderBackButtonIconFile = "X_axis.svg";
 
     static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup)
     {
@@ -62,6 +65,7 @@ namespace AzToolsFramework::ViewportUi::Internal
         , m_fullScreenLayout(&m_uiOverlay)
         , m_uiOverlayLayout()
         , m_viewportBorderText(&m_uiOverlay)
+        , m_viewportBorderBackButton(&m_uiOverlay)
     {
     }
 
@@ -291,7 +295,8 @@ namespace AzToolsFramework::ViewportUi::Internal
         return false;
     }
 
-    void ViewportUiDisplay::CreateViewportBorder(const AZStd::string& borderTitle)
+    void ViewportUiDisplay::CreateViewportBorder(
+        const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback)
     {
         const AZStd::string styleSheet = AZStd::string::format(
             "border: %dpx solid %s; border-top: %dpx solid %s;", HighlightBorderSize, HighlightBorderColor, ViewportUiTopBorderSize,
@@ -302,6 +307,10 @@ namespace AzToolsFramework::ViewportUi::Internal
             HighlightBorderSize + ViewportUiOverlayMargin, HighlightBorderSize + ViewportUiOverlayMargin);
         m_viewportBorderText.setVisible(true);
         m_viewportBorderText.setText(borderTitle.c_str());
+
+        // only display the back button if a callback was provided
+        m_viewportBorderBackButtonCallback = backButtonCallback;
+        m_viewportBorderBackButton.setVisible(m_viewportBorderBackButtonCallback.has_value());
     }
 
     void ViewportUiDisplay::RemoveViewportBorder()
@@ -311,6 +320,8 @@ namespace AzToolsFramework::ViewportUi::Internal
         m_uiOverlayLayout.setContentsMargins(
             ViewportUiOverlayMargin, ViewportUiOverlayMargin + ViewportUiOverlayTopMarginPadding, ViewportUiOverlayMargin,
             ViewportUiOverlayMargin);
+        m_viewportBorderBackButtonCallback.reset();
+        m_viewportBorderBackButton.setVisible(false);
     }
 
     void ViewportUiDisplay::PositionViewportUiElementFromWorldSpace(ViewportUiElementId elementId, const AZ::Vector3& pos)
@@ -347,6 +358,8 @@ namespace AzToolsFramework::ViewportUi::Internal
 
     void ViewportUiDisplay::InitializeUiOverlay()
     {
+        AZStd::string styleSheet;
+
         m_uiMainWindow.setObjectName(QString("ViewportUiWindow"));
         ConfigureWindowForViewportUi(&m_uiMainWindow);
         m_uiMainWindow.setVisible(false);
@@ -361,11 +374,37 @@ namespace AzToolsFramework::ViewportUi::Internal
         m_fullScreenLayout.addLayout(&m_uiOverlayLayout, 0, 0, 1, 1);
 
         // format the label which will appear on top of the highlight border
-        AZStd::string styleSheet = AZStd::string::format("background-color: %s; border: none;", HighlightBorderColor);
+        styleSheet = AZStd::string::format("background-color: %s; border: none;", HighlightBorderColor);
         m_viewportBorderText.setStyleSheet(styleSheet.c_str());
         m_viewportBorderText.setFixedHeight(ViewportUiTopBorderSize);
         m_viewportBorderText.setVisible(false);
         m_fullScreenLayout.addWidget(&m_viewportBorderText, 0, 0, Qt::AlignTop | Qt::AlignHCenter);
+
+        // format the back button which will appear in the top right of the highlight border
+        styleSheet = AZStd::string::format(
+            "border: 0px; padding-left: %dpx; padding-right: %dpx", HighlightBorderBackButtonMargin, HighlightBorderBackButtonMargin);
+        m_viewportBorderBackButton.setStyleSheet(styleSheet.c_str());
+        m_viewportBorderBackButton.setVisible(false);
+        QIcon backButtonIcon(QString(AZStd::string::format(":/stylesheet/img/UI20/toolbar/%s", HighlightBorderBackButtonIconFile).c_str()));
+        m_viewportBorderBackButton.setIcon(backButtonIcon);
+        m_viewportBorderBackButton.setIconSize(QSize(HighlightBorderBackButtonIconSize, HighlightBorderBackButtonIconSize));
+
+        // setup the handler for the back button to call the user provided callback (if any)
+        QObject::connect(
+            &m_viewportBorderBackButton, &QPushButton::clicked,
+            [this]()
+            {
+                if (m_viewportBorderBackButtonCallback.has_value())
+                {
+                    // we need to swap out the existing back button callback because it will be reset in RemoveViewportBorder()
+                    // so preserve the lifetime with this temporary callback until after the call to RemoveViewportBorder()
+                    AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback;
+                    m_viewportBorderBackButtonCallback.swap(backButtonCallback);
+                    RemoveViewportBorder();
+                    (*backButtonCallback)();
+                }
+            });
+        m_fullScreenLayout.addWidget(&m_viewportBorderBackButton, 0, 0, Qt::AlignTop | Qt::AlignRight);
     }
 
     void ViewportUiDisplay::PrepareWidgetForViewportUi(QPointer<QWidget> widget)

+ 6 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.h

@@ -17,6 +17,7 @@
 #include <QLabel>
 #include <QMainWindow>
 #include <QPointer>
+#include <QPushButton>
 
 AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option")
 #include <QGridLayout>
@@ -89,7 +90,7 @@ namespace AzToolsFramework::ViewportUi::Internal
         AZStd::shared_ptr<QWidget> GetViewportUiElement(ViewportUiElementId elementId);
         bool IsViewportUiElementVisible(ViewportUiElementId elementId);
 
-        void CreateViewportBorder(const AZStd::string& borderTitle);
+        void CreateViewportBorder(const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback);
         void RemoveViewportBorder();
 
     private:
@@ -113,7 +114,10 @@ namespace AzToolsFramework::ViewportUi::Internal
         QWidget m_uiOverlay; //!< The UI Overlay which displays Viewport UI Elements.
         QGridLayout m_fullScreenLayout; //!< The layout which extends across the full screen.
         ViewportUiDisplayLayout m_uiOverlayLayout; //!< The layout used for optionally anchoring Viewport UI Elements.
-        QLabel m_viewportBorderText; //!< The text used for the viewport border.
+        QLabel m_viewportBorderText; //!< The text used for the viewport highlight border.
+        QPushButton m_viewportBorderBackButton; //!< The button to return from the viewport highlight border (only displayed if callback provided).
+        AZStd::optional<ViewportUiBackButtonCallback>
+            m_viewportBorderBackButtonCallback; //!< The optional callback for when the viewport highlight border back button is pressed.
 
         QWidget* m_renderOverlay;
         QPointer<QWidget> m_fullScreenWidget; //!< Reference to the widget attached to m_fullScreenLayout if any.

+ 3 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiManager.cpp

@@ -240,9 +240,10 @@ namespace AzToolsFramework::ViewportUi
         }
     }
 
-    void ViewportUiManager::CreateViewportBorder(const AZStd::string& borderTitle)
+    void ViewportUiManager::CreateViewportBorder(
+        const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback)
     {
-        m_viewportUi->CreateViewportBorder(borderTitle);
+        m_viewportUi->CreateViewportBorder(borderTitle, backButtonCallback);
     }
 
     void ViewportUiManager::RemoveViewportBorder()

+ 2 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiManager.h

@@ -50,7 +50,8 @@ namespace AzToolsFramework::ViewportUi
         void RegisterTextFieldCallback(TextFieldId textFieldId, AZ::Event<AZStd::string>::Handler& handler) override;
         void RemoveTextField(TextFieldId textFieldId) override;
         void SetTextFieldVisible(TextFieldId textFieldId, bool visible) override;
-        void CreateViewportBorder(const AZStd::string& borderTitle) override;
+        void CreateViewportBorder(
+            const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback) override;
         void RemoveViewportBorder() override;
         void PressButton(ClusterId clusterId, ButtonId buttonId) override;
         void PressButton(SwitcherId switcherId, ButtonId buttonId) override;

+ 6 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiRequestBus.h

@@ -22,6 +22,9 @@ namespace AzToolsFramework::ViewportUi
     using SwitcherId = IdType<struct SwitcherIdType>;
     using TextFieldId = IdType<struct TextFieldIdType>;
 
+    //! Callback function for viewport UI back button.
+    using ViewportUiBackButtonCallback = AZStd::function<void()>;
+
     inline const ViewportUiElementId InvalidViewportUiElementId = ViewportUiElementId(0);
     inline const ButtonId InvalidButtonId = ButtonId(0);
     inline const ClusterId InvalidClusterId = ClusterId(0);
@@ -95,9 +98,9 @@ namespace AzToolsFramework::ViewportUi
         virtual void RemoveTextField(TextFieldId textFieldId) = 0;
         //! Sets the visibility of the text field.
         virtual void SetTextFieldVisible(TextFieldId textFieldId, bool visible) = 0;
-        //! Create the highlight border for Component Mode.
-        virtual void CreateViewportBorder(const AZStd::string& borderTitle) = 0;
-        //! Remove the highlight border for Component Mode.
+        //! Create the highlight border with optional back button to exit the given editor mode.
+        virtual void CreateViewportBorder(const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback) = 0;
+        //! Remove the highlight border.
         virtual void RemoveViewportBorder() = 0;
         //! Invoke a button press on a cluster.
         virtual void PressButton(ClusterId clusterId, ButtonId buttonId) = 0;

+ 0 - 1
Code/Tools/AssetProcessor/native/utilities/assetUtils.h

@@ -238,7 +238,6 @@ namespace AssetUtilities
     // hashMsDelay is only for automated tests to test that writing to a file while it's hashing does not cause a crash.
     // hashMsDelay is not used in non-unit test builds.
     AZ::u64 GetFileHash(const char* filePath, bool force = false, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
-    inline constexpr AZ::u64 FileHashBufferSize = 1024 * 64;
 
     //! Adjusts a timestamp to fix timezone settings and account for any precision adjustment needed
     AZ::u64 AdjustTimestamp(QDateTime timestamp);

+ 1 - 0
Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp

@@ -15,6 +15,7 @@
 #include <GemCatalog/GemCatalogScreen.h>
 #include <GemRepo/GemRepoScreen.h>
 #include <ProjectUtils.h>
+#include <DownloadController.h>
 
 #include <QDialogButtonBox>
 #include <QHBoxLayout>

+ 1 - 6
Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp

@@ -72,12 +72,7 @@ namespace O3DE::ProjectManager
 
     bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen)
     {
-        if (screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum())
-        {
-            return true;
-        }
-
-        return false;
+        return screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum();
     }
 
     void EngineScreenCtrl::NotifyCurrentScreen()

+ 91 - 43
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp

@@ -7,25 +7,32 @@
  */
 
 #include <GemCatalog/GemCatalogHeaderWidget.h>
+#include <TagWidget.h>
+
 #include <AzCore/std/functional.h>
+
 #include <QHBoxLayout>
 #include <QMouseEvent>
 #include <QLabel>
 #include <QPushButton>
 #include <QProgressBar>
-#include <TagWidget.h>
 #include <QMenu>
 #include <QLocale>
 #include <QMovie>
+#include <QPainter>
+#include <QPainterPath>
 
 namespace O3DE::ProjectManager
 {
-    CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
-        : QWidget(parent)
+    GemCartWidget::GemCartWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
+        : QScrollArea(parent)
         , m_gemModel(gemModel)
         , m_downloadController(downloadController)
     {
         setObjectName("GemCatalogCart");
+        setWidgetResizable(true);
+        setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+        setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
 
         m_layout = new QVBoxLayout();
         m_layout->setSpacing(0);
@@ -118,17 +125,15 @@ namespace O3DE::ProjectManager
                 }
                 return dependencies;
             });
-
-        setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
     }
 
-    CartOverlayWidget::~CartOverlayWidget()
+    GemCartWidget::~GemCartWidget()
     {
         // disconnect from all download controller signals
         disconnect(m_downloadController, nullptr, this, nullptr);
     }
 
-    void CartOverlayWidget::CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices)
+    void GemCartWidget::CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices)
     {
         QWidget* widget = new QWidget();
         widget->setFixedWidth(s_width);
@@ -164,12 +169,12 @@ namespace O3DE::ProjectManager
         update();
     }
 
-    void CartOverlayWidget::OnCancelDownloadActivated(const QString& gemName)
+    void GemCartWidget::OnCancelDownloadActivated(const QString& gemName)
     {
         m_downloadController->CancelGemDownload(gemName);
     }
 
-    void CartOverlayWidget::CreateDownloadSection()
+    void GemCartWidget::CreateDownloadSection()
     {
         m_downloadSectionWidget = new QWidget();
         m_downloadSectionWidget->setFixedWidth(s_width);
@@ -223,12 +228,12 @@ namespace O3DE::ProjectManager
         }
 
         // connect to download controller data changed
-        connect(m_downloadController, &DownloadController::GemDownloadAdded, this, &CartOverlayWidget::GemDownloadAdded);
-        connect(m_downloadController, &DownloadController::GemDownloadRemoved, this, &CartOverlayWidget::GemDownloadRemoved);
-        connect(m_downloadController, &DownloadController::GemDownloadProgress, this, &CartOverlayWidget::GemDownloadProgress);
+        connect(m_downloadController, &DownloadController::GemDownloadAdded, this, &GemCartWidget::GemDownloadAdded);
+        connect(m_downloadController, &DownloadController::GemDownloadRemoved, this, &GemCartWidget::GemDownloadRemoved);
+        connect(m_downloadController, &DownloadController::GemDownloadProgress, this, &GemCartWidget::GemDownloadProgress);
     }
 
-    void CartOverlayWidget::GemDownloadAdded(const QString& gemName)
+    void GemCartWidget::GemDownloadAdded(const QString& gemName)
     {
         // Containing widget for the current download item
         QWidget* newGemDownloadWidget = new QWidget();
@@ -246,7 +251,7 @@ namespace O3DE::ProjectManager
         nameProgressLayout->addStretch();
         QLabel* cancelText = new QLabel(tr("<a href=\"%1\">Cancel</a>").arg(gemName), newGemDownloadWidget);
         cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
-        connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated);
+        connect(cancelText, &QLabel::linkActivated, this, &GemCartWidget::OnCancelDownloadActivated);
         nameProgressLayout->addWidget(cancelText);
         downloadingGemLayout->addLayout(nameProgressLayout);
 
@@ -267,7 +272,7 @@ namespace O3DE::ProjectManager
         m_downloadingListWidget->show();
     }
 
-    void CartOverlayWidget::GemDownloadRemoved(const QString& gemName)
+    void GemCartWidget::GemDownloadRemoved(const QString& gemName)
     {
         QWidget* gemToRemove = m_downloadingListWidget->findChild<QWidget*>(gemName);
         if (gemToRemove)
@@ -289,7 +294,7 @@ namespace O3DE::ProjectManager
         }
     }
 
-    void CartOverlayWidget::GemDownloadProgress(const QString& gemName, int bytesDownloaded, int totalBytes)
+    void GemCartWidget::GemDownloadProgress(const QString& gemName, int bytesDownloaded, int totalBytes)
     {
         QWidget* gemToUpdate = m_downloadingListWidget->findChild<QWidget*>(gemName);
         if (gemToUpdate)
@@ -324,7 +329,7 @@ namespace O3DE::ProjectManager
         }
     }
 
-    QVector<Tag> CartOverlayWidget::GetTagsFromModelIndices(const QVector<QModelIndex>& gems) const
+    QVector<Tag> GemCartWidget::GetTagsFromModelIndices(const QVector<QModelIndex>& gems) const
     {
         QVector<Tag> tags;
         tags.reserve(gems.size());
@@ -349,7 +354,7 @@ namespace O3DE::ProjectManager
         iconButton->setFocusPolicy(Qt::NoFocus);
         iconButton->setIcon(QIcon(":/Summary.svg"));
         iconButton->setFixedSize(s_iconSize, s_iconSize);
-        connect(iconButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
+        connect(iconButton, &QPushButton::clicked, this, &CartButton::ShowGemCart);
         m_layout->addWidget(iconButton);
 
         m_countLabel = new QLabel();
@@ -362,7 +367,7 @@ namespace O3DE::ProjectManager
         m_dropDownButton->setFocusPolicy(Qt::NoFocus);
         m_dropDownButton->setIcon(QIcon(":/CarrotArrowDown.svg"));
         m_dropDownButton->setFixedSize(s_arrowDownIconSize, s_arrowDownIconSize);
-        connect(m_dropDownButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
+        connect(m_dropDownButton, &QPushButton::clicked, this, &CartButton::ShowGemCart);
         m_layout->addWidget(m_dropDownButton);
 
         // Adjust the label text whenever the model gets updated.
@@ -377,28 +382,28 @@ namespace O3DE::ProjectManager
                 m_dropDownButton->setVisible(!toBeAdded.isEmpty() || !toBeRemoved.isEmpty());
 
                 // Automatically close the overlay window in case there are no gems to be activated or deactivated anymore.
-                if (m_cartOverlay && toBeAdded.isEmpty() && toBeRemoved.isEmpty())
+                if (m_gemCart && toBeAdded.isEmpty() && toBeRemoved.isEmpty())
                 {
-                    m_cartOverlay->deleteLater();
-                    m_cartOverlay = nullptr;
+                    m_gemCart->deleteLater();
+                    m_gemCart = nullptr;
                 }
             });
     }
 
     void CartButton::mousePressEvent([[maybe_unused]] QMouseEvent* event)
     {
-        ShowOverlay();
+        ShowGemCart();
     }
 
     void CartButton::hideEvent(QHideEvent*)
     {
-        if (m_cartOverlay)
+        if (m_gemCart)
         {
-            m_cartOverlay->hide();
+            m_gemCart->hide();
         }
     }
 
-    void CartButton::ShowOverlay()
+    void CartButton::ShowGemCart()
     {
         const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded(/*includeDependencies=*/true);
         const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved(/*includeDependencies=*/true);
@@ -407,37 +412,33 @@ namespace O3DE::ProjectManager
             return;
         }
 
-        if (m_cartOverlay)
+        if (m_gemCart)
         {
             // Directly delete the former overlay before creating the new one.
             // Don't use deleteLater() here. This might overwrite the new overlay pointer
             // depending on the event queue.
-            delete m_cartOverlay;
+            delete m_gemCart;
         }
 
-        m_cartOverlay = new CartOverlayWidget(m_gemModel, m_downloadController, this);
-        connect(m_cartOverlay, &QWidget::destroyed, this, [=]
+        m_gemCart = new GemCartWidget(m_gemModel, m_downloadController, this);
+        connect(m_gemCart, &QWidget::destroyed, this, [=]
             {
                 // Reset the overlay pointer on destruction to prevent dangling pointers.
-                m_cartOverlay = nullptr;
+                m_gemCart = nullptr;
+                // Tell header gem cart is no longer open
+                UpdateGemCart(nullptr);
             });
-        m_cartOverlay->show();
-
-        const QPoint parentPos = m_dropDownButton->mapToParent(m_dropDownButton->pos());
-        const QPoint globalPos = m_dropDownButton->mapToGlobal(m_dropDownButton->pos());
-        const QPoint offset(-4, 10);
-        m_cartOverlay->setGeometry(globalPos.x() - parentPos.x() - m_cartOverlay->width() + width() + offset.x(),
-            globalPos.y() + offset.y(),
-            m_cartOverlay->width(),
-            m_cartOverlay->height());
+        m_gemCart->show();
+
+        emit UpdateGemCart(m_gemCart);
     }
 
     CartButton::~CartButton()
     {
         // Make sure the overlay window is automatically closed in case the gem catalog is destroyed.
-        if (m_cartOverlay)
+        if (m_gemCart)
         {
-            m_cartOverlay->deleteLater();
+            m_gemCart->deleteLater();
         }
     }
 
@@ -514,6 +515,17 @@ namespace O3DE::ProjectManager
 
         connect(m_downloadController, &DownloadController::GemDownloadAdded, this, &GemCatalogHeaderWidget::GemDownloadAdded);
         connect(m_downloadController, &DownloadController::GemDownloadRemoved, this, &GemCatalogHeaderWidget::GemDownloadRemoved);
+
+        connect(
+            m_cartButton, &CartButton::UpdateGemCart, this,
+            [this](QWidget* gemCart)
+            {
+                GemCartShown(gemCart);
+                if (gemCart)
+                {
+                    emit UpdateGemCart(gemCart);
+                }
+            });
     }
 
     void GemCatalogHeaderWidget::GemDownloadAdded(const QString& /*gemName*/)
@@ -521,7 +533,7 @@ namespace O3DE::ProjectManager
         m_downloadSpinner->show();
         m_downloadLabel->show();
         m_downloadSpinnerMovie->start();
-        m_cartButton->ShowOverlay();
+        m_cartButton->ShowGemCart();
     }
 
     void GemCatalogHeaderWidget::GemDownloadRemoved(const QString& /*gemName*/)
@@ -534,8 +546,44 @@ namespace O3DE::ProjectManager
         }
     }
 
+    void GemCatalogHeaderWidget::GemCartShown(bool state)
+    {
+        m_showGemCart = state;
+        repaint();
+    }
+
     void GemCatalogHeaderWidget::ReinitForProject()
     {
         m_filterLineEdit->setText({});
     }
+
+    void GemCatalogHeaderWidget::paintEvent([[maybe_unused]] QPaintEvent* event)
+    {
+        // Only show triangle when cart is shown
+        if (!m_showGemCart)
+        {
+            return;
+        }
+
+        const QPoint buttonPos = m_cartButton->pos();
+        const QSize buttonSize = m_cartButton->size();
+
+        // Draw isosceles triangle with top point touching bottom of cartButton
+        // Bottom aligned with header bottom and top of right panel
+        const QPoint topPoint(buttonPos.x() + buttonSize.width() / 2, buttonPos.y() + buttonSize.height());
+        const QPoint bottomLeftPoint(topPoint.x() - 20, height());
+        const QPoint bottomRightPoint(topPoint.x() + 20, height());
+
+        QPainterPath trianglePath;
+        trianglePath.moveTo(topPoint);
+        trianglePath.lineTo(bottomLeftPoint);
+        trianglePath.lineTo(bottomRightPoint);
+        trianglePath.lineTo(topPoint);
+
+        QPainter painter(this);
+        painter.setRenderHint(QPainter::Antialiasing, true);
+        painter.setPen(Qt::NoPen);
+        painter.fillPath(trianglePath, QBrush(QColor("#555555")));
+    }
+
 } // namespace O3DE::ProjectManager

+ 18 - 7
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h

@@ -14,8 +14,10 @@
 #include <GemCatalog/GemModel.h>
 #include <GemCatalog/GemSortFilterProxyModel.h>
 #include <TagWidget.h>
-#include <QFrame>
 #include <DownloadController.h>
+
+#include <QFrame>
+#include <QScrollArea>
 #endif
 
 QT_FORWARD_DECLARE_CLASS(QPushButton)
@@ -28,14 +30,14 @@ QT_FORWARD_DECLARE_CLASS(QMovie)
 
 namespace O3DE::ProjectManager
 {
-    class CartOverlayWidget
-        : public QWidget
+    class GemCartWidget
+        : public QScrollArea
     {
         Q_OBJECT // AUTOMOC
 
     public:
-        CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
-        ~CartOverlayWidget();
+        GemCartWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
+        ~GemCartWidget();
 
     public slots:
         void GemDownloadAdded(const QString& gemName);
@@ -68,7 +70,10 @@ namespace O3DE::ProjectManager
     public:
         CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
         ~CartButton();
-        void ShowOverlay();
+        void ShowGemCart();
+
+    signals:
+        void UpdateGemCart(QWidget* gemCart);
 
     private:
         void mousePressEvent(QMouseEvent* event) override;
@@ -78,7 +83,7 @@ namespace O3DE::ProjectManager
         QHBoxLayout* m_layout = nullptr;
         QLabel* m_countLabel = nullptr;
         QPushButton* m_dropDownButton = nullptr;
-        CartOverlayWidget* m_cartOverlay = nullptr;
+        GemCartWidget* m_gemCart = nullptr;
         DownloadController* m_downloadController = nullptr;
 
         inline constexpr static int s_iconSize = 24;
@@ -99,11 +104,16 @@ namespace O3DE::ProjectManager
     public slots:
         void GemDownloadAdded(const QString& gemName);
         void GemDownloadRemoved(const QString& gemName);
+        void GemCartShown(bool state = false);
 
     signals:
         void AddGem();
         void OpenGemsRepo();
         void RefreshGems();
+        void UpdateGemCart(QWidget* gemCart);
+
+    protected slots:
+        void paintEvent(QPaintEvent* event) override;
         
     private:
         AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr;
@@ -113,5 +123,6 @@ namespace O3DE::ProjectManager
         QLabel* m_downloadLabel = nullptr;
         QMovie* m_downloadSpinnerMovie = nullptr;
         CartButton* m_cartButton = nullptr;
+        bool m_showGemCart = false;
     };
 } // namespace O3DE::ProjectManager

+ 37 - 4
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

@@ -8,6 +8,11 @@
 
 #include <GemCatalog/GemCatalogScreen.h>
 #include <PythonBindingsInterface.h>
+#include <GemCatalog/GemCatalogHeaderWidget.h>
+#include <GemCatalog/GemFilterWidget.h>
+#include <GemCatalog/GemListView.h>
+#include <GemCatalog/GemInspector.h>
+#include <GemCatalog/GemModel.h>
 #include <GemCatalog/GemListHeaderWidget.h>
 #include <GemCatalog/GemSortFilterProxyModel.h>
 #include <GemCatalog/GemRequirementDialog.h>
@@ -28,6 +33,7 @@
 #include <QFileDialog>
 #include <QMessageBox>
 #include <QHash>
+#include <QStackedWidget>
 
 namespace O3DE::ProjectManager
 {
@@ -51,9 +57,11 @@ namespace O3DE::ProjectManager
         vLayout->addWidget(m_headerWidget);
 
         connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged);
+        connect(m_gemModel->GetSelectionModel(), &QItemSelectionModel::selectionChanged, this, [this]{ ShowInspector(); });
         connect(m_headerWidget, &GemCatalogHeaderWidget::RefreshGems, this, &GemCatalogScreen::Refresh);
         connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo);
         connect(m_headerWidget, &GemCatalogHeaderWidget::AddGem, this, &GemCatalogScreen::OnAddGemClicked);
+        connect(m_headerWidget, &GemCatalogHeaderWidget::UpdateGemCart, this, &GemCatalogScreen::UpdateAndShowGemCart);
         connect(m_downloadController, &DownloadController::Done, this, &GemCatalogScreen::OnGemDownloadResult);
 
         QHBoxLayout* hLayout = new QHBoxLayout();
@@ -61,8 +69,11 @@ namespace O3DE::ProjectManager
         vLayout->addLayout(hLayout);
 
         m_gemListView = new GemListView(m_proxyModel, m_proxyModel->GetSelectionModel(), this);
+
+        m_rightPanelStack = new QStackedWidget(this);
+        m_rightPanelStack->setFixedWidth(240);
+
         m_gemInspector = new GemInspector(m_gemModel, this);
-        m_gemInspector->setFixedWidth(240);
 
         connect(m_gemInspector, &GemInspector::TagClicked, [=](const Tag& tag) { SelectGem(tag.id); });
         connect(m_gemInspector, &GemInspector::UpdateGem, this, &GemCatalogScreen::UpdateGem);
@@ -85,7 +96,9 @@ namespace O3DE::ProjectManager
 
         hLayout->addWidget(filterWidget);
         hLayout->addLayout(middleVLayout);
-        hLayout->addWidget(m_gemInspector);
+
+        hLayout->addWidget(m_rightPanelStack);
+        m_rightPanelStack->addWidget(m_gemInspector);
 
         m_notificationsView = AZStd::make_unique<AzToolsFramework::ToastNotificationsView>(this, AZ_CRC("GemCatalogNotificationsView"));
         m_notificationsView->SetOffset(QPoint(10, 70));
@@ -188,7 +201,7 @@ namespace O3DE::ProjectManager
         }
 
         // add all the gem repos into the hash
-        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemReposGemInfos();
+        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
         if (allRepoGemInfosResult.IsSuccess())
         {
             const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@@ -303,6 +316,8 @@ namespace O3DE::ProjectManager
         QModelIndex proxyIndex = m_proxyModel->mapFromSource(modelIndex);
         m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
         m_gemListView->scrollTo(proxyIndex);
+
+        ShowInspector();
     }
 
     void GemCatalogScreen::UpdateGem(const QModelIndex& modelIndex)
@@ -440,7 +455,7 @@ namespace O3DE::ProjectManager
                 m_gemModel->AddGem(gemInfo);
             }
 
-            const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemReposGemInfos();
+            const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
             if (allRepoGemInfosResult.IsSuccess())
             {
                 const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@@ -496,6 +511,12 @@ namespace O3DE::ProjectManager
         }
     }
 
+    void GemCatalogScreen::ShowInspector()
+    {
+        m_rightPanelStack->setCurrentIndex(RightPanelWidgetOrder::Inspector);
+        m_headerWidget->GemCartShown();
+    }
+
     GemCatalogScreen::EnableDisableGemsResult GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
     {
         IPythonBindings* pythonBindings = PythonBindingsInterface::Get();
@@ -577,6 +598,18 @@ namespace O3DE::ProjectManager
         emit ChangeScreenRequest(ProjectManagerScreen::GemRepos);
     }
 
+    void GemCatalogScreen::UpdateAndShowGemCart(QWidget* cartWidget)
+    {
+        QWidget* previousCart = m_rightPanelStack->widget(RightPanelWidgetOrder::Cart);
+        if (previousCart)
+        {
+            m_rightPanelStack->removeWidget(previousCart);
+        }
+
+        m_rightPanelStack->insertWidget(RightPanelWidgetOrder::Cart, cartWidget);
+        m_rightPanelStack->setCurrentIndex(RightPanelWidgetOrder::Cart);
+    }
+
     void GemCatalogScreen::OnGemDownloadResult(const QString& gemName, bool succeeded)
     {
         if (succeeded)

+ 21 - 7
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h

@@ -12,18 +12,24 @@
 #include <ScreenWidget.h>
 #include <AzCore/std/smart_ptr/unique_ptr.h>
 #include <AzToolsFramework/UI/Notifications/ToastNotificationsView.h>
-#include <GemCatalog/GemCatalogHeaderWidget.h>
-#include <GemCatalog/GemFilterWidget.h>
-#include <GemCatalog/GemListView.h>
-#include <GemCatalog/GemInspector.h>
-#include <GemCatalog/GemModel.h>
-#include <GemCatalog/GemSortFilterProxyModel.h>
+
 #include <QSet>
 #include <QString>
 #endif
 
+QT_FORWARD_DECLARE_CLASS(QVBoxLayout)
+QT_FORWARD_DECLARE_CLASS(QStackedWidget)
+
 namespace O3DE::ProjectManager
 {
+    QT_FORWARD_DECLARE_CLASS(GemCatalogHeaderWidget)
+    QT_FORWARD_DECLARE_CLASS(GemFilterWidget)
+    QT_FORWARD_DECLARE_CLASS(GemListView)
+    QT_FORWARD_DECLARE_CLASS(GemInspector)
+    QT_FORWARD_DECLARE_CLASS(GemModel)
+    QT_FORWARD_DECLARE_CLASS(GemSortFilterProxyModel)
+    QT_FORWARD_DECLARE_CLASS(DownloadController)
+
     class GemCatalogScreen
         : public ScreenWidget
     {
@@ -62,14 +68,22 @@ namespace O3DE::ProjectManager
 
     private slots:
         void HandleOpenGemRepo();
-
+        void UpdateAndShowGemCart(QWidget* cartWidget);
+        void ShowInspector();
 
     private:
+        enum RightPanelWidgetOrder
+        {
+            Inspector = 0,
+            Cart
+        };
+
         void FillModel(const QString& projectPath);
 
         AZStd::unique_ptr<AzToolsFramework::ToastNotificationsView> m_notificationsView;
 
         GemListView* m_gemListView = nullptr;
+        QStackedWidget* m_rightPanelStack = nullptr;
         GemInspector* m_gemInspector = nullptr;
         GemModel* m_gemModel = nullptr;
         GemCatalogHeaderWidget* m_headerWidget = nullptr;

+ 9 - 5
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp

@@ -53,10 +53,13 @@ namespace O3DE::ProjectManager
         Update(selectedIndices[0]);
     }
 
-    void SetLabelElidedText(QLabel* label, QString text)
+    void SetLabelElidedText(QLabel* label, QString text, int labelWidth = 0)
     {
         QFontMetrics nameFontMetrics(label->font());
-        int labelWidth = label->width();
+        if (!labelWidth)
+        {
+            labelWidth = label->width();
+        }
 
         // Don't elide if the widgets are sized too small (sometimes occurs when loading gem catalog)
         if (labelWidth > 100)
@@ -84,7 +87,8 @@ namespace O3DE::ProjectManager
         m_summaryLabel->setText(m_model->GetSummary(modelIndex));
         m_summaryLabel->adjustSize();
 
-        m_licenseLinkLabel->setText(m_model->GetLicenseText(modelIndex));
+        // Manually define remaining space to elide text because spacer would like to take all of the space
+        SetLabelElidedText(m_licenseLinkLabel, m_model->GetLicenseText(modelIndex), width() - m_licenseLabel->width() - 35);
         m_licenseLinkLabel->SetUrl(m_model->GetLicenseLink(modelIndex));
 
         m_directoryLinkLabel->SetUrl(m_model->GetDirectoryLink(modelIndex));
@@ -175,8 +179,8 @@ namespace O3DE::ProjectManager
             licenseHLayout->setAlignment(Qt::AlignLeft);
             m_mainLayout->addLayout(licenseHLayout);
 
-            QLabel* licenseLabel = CreateStyledLabel(licenseHLayout, s_baseFontSize, s_headerColor);
-            licenseLabel->setText(tr("License: "));
+            m_licenseLabel = CreateStyledLabel(licenseHLayout, s_baseFontSize, s_headerColor);
+            m_licenseLabel->setText(tr("License: "));
 
             m_licenseLinkLabel = new LinkLabel("", QUrl(), s_baseFontSize);
             licenseHLayout->addWidget(m_licenseLinkLabel);

+ 1 - 0
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h

@@ -64,6 +64,7 @@ namespace O3DE::ProjectManager
         QLabel* m_nameLabel = nullptr;
         QLabel* m_creatorLabel = nullptr;
         QLabel* m_summaryLabel = nullptr;
+        QLabel* m_licenseLabel = nullptr;
         LinkLabel* m_licenseLinkLabel = nullptr;
         LinkLabel* m_directoryLinkLabel = nullptr;
         LinkLabel* m_documentationLinkLabel = nullptr;

+ 1 - 1
Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.cpp

@@ -120,7 +120,7 @@ namespace O3DE::ProjectManager
     {
         QString repoUri = GetRepoUri(modelIndex);
 
-        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& gemInfosResult = PythonBindingsInterface::Get()->GetGemRepoGemInfos(repoUri);
+        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& gemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForRepo(repoUri);
         if (gemInfosResult.IsSuccess())
         {
             return gemInfosResult.GetValue();

+ 1 - 0
Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp

@@ -203,6 +203,7 @@ namespace O3DE::ProjectManager
 
             QMenu* menu = new QMenu(this);
             menu->addAction(tr("Edit Project Settings..."), this, [this]() { emit EditProject(m_projectInfo.m_path); });
+            menu->addAction(tr("Configure Gems..."), this, [this]() { emit EditProjectGems(m_projectInfo.m_path); });
             menu->addAction(tr("Build"), this, [this]() { emit BuildProject(m_projectInfo); });
             menu->addAction(tr("Open CMake GUI..."), this, [this]() { emit OpenCMakeGUI(m_projectInfo); });
             menu->addSeparator();

+ 1 - 0
Code/Tools/ProjectManager/Source/ProjectButtonWidget.h

@@ -95,6 +95,7 @@ namespace O3DE::ProjectManager
     signals:
         void OpenProject(const QString& projectName);
         void EditProject(const QString& projectName);
+        void EditProjectGems(const QString& projectName);
         void CopyProject(const ProjectInfo& projectInfo);
         void RemoveProject(const QString& projectName);
         void DeleteProject(const QString& projectName);

+ 16 - 3
Code/Tools/ProjectManager/Source/ProjectSettingsScreen.cpp

@@ -19,6 +19,7 @@
 #include <QLabel>
 #include <QLineEdit>
 #include <QStandardPaths>
+#include <QScrollArea>
 
 namespace O3DE::ProjectManager
 {
@@ -33,11 +34,23 @@ namespace O3DE::ProjectManager
         // if we don't set this in a frame (just use a sub-layout) all the content will align incorrectly horizontally
         QFrame* projectSettingsFrame = new QFrame(this);
         projectSettingsFrame->setObjectName("projectSettings");
-        m_verticalLayout = new QVBoxLayout();
 
-        // you cannot remove content margins in qss
-        m_verticalLayout->setContentsMargins(0, 0, 0, 0);
+        QVBoxLayout* vLayout = new QVBoxLayout();
+        vLayout->setMargin(0);
+        vLayout->setAlignment(Qt::AlignTop);
+        projectSettingsFrame->setLayout(vLayout);
+
+        QScrollArea* scrollArea = new QScrollArea(this);
+        scrollArea->setWidgetResizable(true);
+        vLayout->addWidget(scrollArea);
+
+        QWidget* scrollWidget = new QWidget(this);
+        scrollArea->setWidget(scrollWidget);
+
+        m_verticalLayout = new QVBoxLayout();
+        m_verticalLayout->setMargin(0);
         m_verticalLayout->setAlignment(Qt::AlignTop);
+        scrollWidget->setLayout(m_verticalLayout);
 
         m_projectName = new FormLineEditWidget(tr("Project name"), "", this);
         connect(m_projectName->lineEdit(), &QLineEdit::textChanged, this, &ProjectSettingsScreen::OnProjectNameUpdated);

+ 9 - 0
Code/Tools/ProjectManager/Source/ProjectsScreen.cpp

@@ -183,6 +183,7 @@ namespace O3DE::ProjectManager
 
         connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
         connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
+        connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems);
         connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
         connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
         connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
@@ -469,6 +470,14 @@ namespace O3DE::ProjectManager
             emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
         }
     }
+    void ProjectsScreen::HandleEditProjectGems(const QString& projectPath)
+    {
+        if (!WarnIfInBuildQueue(projectPath))
+        {
+            emit NotifyCurrentProject(projectPath);
+            emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog);
+        }
+    }
     void ProjectsScreen::HandleCopyProject(const ProjectInfo& projectInfo)
     {
         if (!WarnIfInBuildQueue(projectInfo.m_path))

+ 1 - 0
Code/Tools/ProjectManager/Source/ProjectsScreen.h

@@ -46,6 +46,7 @@ namespace O3DE::ProjectManager
         void HandleAddProjectButton();
         void HandleOpenProject(const QString& projectPath);
         void HandleEditProject(const QString& projectPath);
+        void HandleEditProjectGems(const QString& projectPath);
         void HandleCopyProject(const ProjectInfo& projectInfo);
         void HandleRemoveProject(const QString& projectPath);
         void HandleDeleteProject(const QString& projectPath);

+ 2 - 2
Code/Tools/ProjectManager/Source/PythonBindings.cpp

@@ -1206,7 +1206,7 @@ namespace O3DE::ProjectManager
         return AZ::Success(AZStd::move(gemRepos));
     }
 
-    AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetGemRepoGemInfos(const QString& repoUri)
+    AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetGemInfosForRepo(const QString& repoUri)
     {
         QVector<GemInfo> gemInfos;
         AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
@@ -1234,7 +1234,7 @@ namespace O3DE::ProjectManager
         return AZ::Success(AZStd::move(gemInfos));
     }
 
-    AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetAllGemReposGemInfos()
+    AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetGemInfosForAllRepos()
     {
         QVector<GemInfo> gemInfos;
         AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(

+ 2 - 2
Code/Tools/ProjectManager/Source/PythonBindings.h

@@ -65,8 +65,8 @@ namespace O3DE::ProjectManager
         AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>> AddGemRepo(const QString& repoUri) override;
         bool RemoveGemRepo(const QString& repoUri) override;
         AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
-        AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemRepoGemInfos(const QString& repoUri) override;
-        AZ::Outcome<QVector<GemInfo>, AZStd::string> GetAllGemReposGemInfos() override;
+        AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForRepo(const QString& repoUri) override;
+        AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForAllRepos() override;
         AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>> DownloadGem(
             const QString& gemName, std::function<void(int, int)> gemProgressCallback, bool force = false) override;
         void CancelDownload() override;

+ 3 - 3
Code/Tools/ProjectManager/Source/PythonBindingsInterface.h

@@ -218,17 +218,17 @@ namespace O3DE::ProjectManager
         virtual AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() = 0;
 
         /**
-         * Gathers all gem infos for repos
+         * Gathers all gem infos from the provided repo
          * @param repoUri the absolute filesystem path or url to the gem repo.
          * @return A list of gem infos.
          */
-        virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemRepoGemInfos(const QString& repoUri) = 0;
+        virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForRepo(const QString& repoUri) = 0;
 
         /**
          * Gathers all gem infos for all gems registered from repos.
          * @return A list of gem infos.
          */
-        virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetAllGemReposGemInfos() = 0;
+        virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForAllRepos() = 0;
 
         /**
          * Downloads and registers a Gem.

+ 2 - 3
Code/Tools/ProjectManager/Source/ScreenWidget.h

@@ -47,9 +47,9 @@ namespace O3DE::ProjectManager
             return tr("Missing");
         }
 
-        virtual bool ContainsScreen([[maybe_unused]] ProjectManagerScreen screen)
+        virtual bool ContainsScreen(ProjectManagerScreen screen)
         {
-            return false;
+            return GetScreenEnum() == screen;
         }
         virtual void GoToScreen([[maybe_unused]] ProjectManagerScreen screen)
         {
@@ -58,7 +58,6 @@ namespace O3DE::ProjectManager
         //! Notify this screen it is the current screen 
         virtual void NotifyCurrentScreen()
         {
-
         }
 
     signals:

+ 22 - 1
Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp

@@ -15,8 +15,8 @@
 #include <UpdateProjectCtrl.h>
 #include <UpdateProjectSettingsScreen.h>
 #include <ProjectUtils.h>
+#include <DownloadController.h>
 #include <ProjectManagerSettings.h>
-
 #include <AzCore/Settings/SettingsRegistry.h>
 
 #include <QDialogButtonBox>
@@ -97,6 +97,17 @@ namespace O3DE::ProjectManager
         return ProjectManagerScreen::UpdateProject;
     }
 
+    bool UpdateProjectCtrl::ContainsScreen(ProjectManagerScreen screen)
+    {
+        // Do not include GemRepos because we don't want to advertise jumping to it from all other screens here
+        return screen == GetScreenEnum() || screen == ProjectManagerScreen::GemCatalog;
+    }
+
+    void UpdateProjectCtrl::GoToScreen(ProjectManagerScreen screen)
+    {
+        OnChangeScreenRequest(screen);
+    }
+
     // Called when pressing "Edit Project Settings..."
     void UpdateProjectCtrl::NotifyCurrentScreen()
     {
@@ -117,6 +128,16 @@ namespace O3DE::ProjectManager
             m_stack->setCurrentWidget(m_gemRepoScreen);
             Update();
         }
+        else if (screen == ProjectManagerScreen::GemCatalog)
+        {
+            m_stack->setCurrentWidget(m_gemCatalogScreen);
+            Update();
+        }
+        else if (screen == ProjectManagerScreen::UpdateProjectSettings)
+        {
+            m_stack->setCurrentWidget(m_updateSettingsScreen);
+            Update();
+        }
         else
         {
             emit ChangeScreenRequest(screen);

+ 4 - 2
Code/Tools/ProjectManager/Source/UpdateProjectCtrl.h

@@ -24,7 +24,8 @@ namespace O3DE::ProjectManager
     QT_FORWARD_DECLARE_CLASS(GemCatalogScreen)
     QT_FORWARD_DECLARE_CLASS(GemRepoScreen)
 
-    class UpdateProjectCtrl : public ScreenWidget
+    class UpdateProjectCtrl
+        : public ScreenWidget
     {
         Q_OBJECT
     public:
@@ -32,7 +33,8 @@ namespace O3DE::ProjectManager
         ~UpdateProjectCtrl() = default;
         ProjectManagerScreen GetScreenEnum() override;
 
-    protected:
+        bool ContainsScreen(ProjectManagerScreen screen) override;
+        void GoToScreen(ProjectManagerScreen screen) override;
         void NotifyCurrentScreen() override;
 
     protected slots:

+ 1 - 1
Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp

@@ -35,7 +35,7 @@ namespace O3DE::ProjectManager
         previewExtrasLayout->setContentsMargins(50, 0, 0, 0);
 
         QLabel* projectPreviewLabel = new QLabel(tr("Select an image (PNG). Minimum %1 x %2 pixels.")
-                           .arg(QString::number(ProjectPreviewImageWidth), QString::number(ProjectPreviewImageHeight)));
+            .arg(QString::number(ProjectPreviewImageWidth), QString::number(ProjectPreviewImageHeight)));
         projectPreviewLabel->setObjectName("projectPreviewLabel");
         previewExtrasLayout->addWidget(projectPreviewLabel);
 

+ 15 - 11
Gems/Atom/Asset/ImageProcessingAtom/Assets/Config/ImageBuilder.settings

@@ -106,20 +106,21 @@
             "_em": [ "Emissive" ],
             "_emit": [ "Emissive" ],
             // displacement
-            "_displ": [ "Emissive" ],
-            "_disp": [ "Emissive" ],
-            "_dsp": [ "Emissive" ],
-            "_d": [ "Emissive" ],
-            "_dm": [ "Emissive" ],
-            "_displacement": [ "Emissive" ],
-            "_height": [ "Emissive" ],
-            "_hm": [ "Emissive" ],
-            "_ht": [ "Emissive" ],
-            "_h": [ "Emissive" ],
+            "_displ": [ "Displacement" ],
+            "_disp": [ "Displacement" ],
+            "_dsp": [ "Displacement" ],
+            "_d": [ "Displacement" ],
+            "_dm": [ "Displacement" ],
+            "_displacement": [ "Displacement" ],
+            "_height": [ "Displacement" ],
+            "_hm": [ "Displacement" ],
+            "_ht": [ "Displacement" ],
+            "_h": [ "Displacement" ],
             // cubemap
             "_ibldiffusecm": [ "IBLDiffuse" ],
             "_iblskyboxcm": [ "IBLSkybox" ],
             "_iblspecularcm": [ "IBLSpecular" ],
+            "_iblspecularcm64": [ "IBLSpecularVeryLow" ],
             "_iblspecularcm128": [ "IBLSpecularLow" ],
             "_iblspecularcm256": [ "IBLSpecular" ],
             "_iblspecularcm512": [ "IBLSpecularHigh" ],
@@ -133,7 +134,10 @@
             // lut
             "_lut": [ "LUT_RG8" ],
             "_lutr32f": [ "LUT_R32F" ],
+            "_lutrgba8": [ "LUT_RGBA8" ],
             "_lutrgba16": [ "LUT_RGBA16" ],
+            "_lutrgba16f": [ "LUT_RGBA16F" ],
+            "_lutrg16": [ "LUT_RG16" ],
             "_lutrg32f": [ "LUT_RG32F" ],
             "_lutrgba32f": [ "LUT_RGBA32F" ],
             // layer mask
@@ -142,7 +146,7 @@
             // decal
             "_decal": [ "Decal_AlbedoWithOpacity" ],
             // ui
-            "_ui": [ "UserInterface_Lossless" ]
+            "_ui": [ "UserInterface_Compressed","UserInterface_Lossless" ]
         },
         "DefaultPreset": "Albedo",
         "DefaultPresetAlpha": "AlbedoWithGenericAlpha"

+ 5 - 3
Gems/Atom/Asset/ImageProcessingAtom/Assets/Config/Reflectance.preset

@@ -8,7 +8,7 @@
             "Name": "Reflectance",
             "SourceColor": "Linear",
             "DestColor": "Linear",
-            "PixelFormat": "BC1",
+            "PixelFormat": "BC4",
             "IsPowerOf2": true,
             "MipMapSetting": {
                 "MipGenType": "Box"
@@ -21,6 +21,7 @@
                 "SourceColor": "Linear",
                 "DestColor": "Linear",
                 "PixelFormat": "ASTC_6x6",
+                "Swizzle": "rrr1",
                 "MaxTextureSize": 2048,
                 "IsPowerOf2": true,
                 "MipMapSetting": {
@@ -33,6 +34,7 @@
                 "SourceColor": "Linear",
                 "DestColor": "Linear",
                 "PixelFormat": "ASTC_6x6",
+                "Swizzle": "rrr1",
                 "MaxTextureSize": 2048,
                 "IsPowerOf2": true,
                 "MipMapSetting": {
@@ -44,7 +46,7 @@
                 "Name": "Reflectance",
                 "SourceColor": "Linear",
                 "DestColor": "Linear",
-                "PixelFormat": "BC1",
+                "PixelFormat": "BC4",
                 "IsPowerOf2": true,
                 "MipMapSetting": {
                     "MipGenType": "Box"
@@ -55,7 +57,7 @@
                 "Name": "Reflectance",
                 "SourceColor": "Linear",
                 "DestColor": "Linear",
-                "PixelFormat": "BC1",
+                "PixelFormat": "BC4",
                 "IsPowerOf2": true,
                 "MipMapSetting": {
                     "MipGenType": "Box"

+ 0 - 1
Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Utils/ModelPreset.h

@@ -30,7 +30,6 @@ namespace AZ
 
             AZStd::string m_displayName;
             AZ::Data::Asset<AZ::RPI::ModelAsset> m_modelAsset;
-            AZ::Data::Asset<AZ::RPI::StreamingImageAsset> m_previewImageAsset;
         };
 
         using ModelPresetPtr = AZStd::shared_ptr<ModelPreset>;

+ 0 - 1
Gems/Atom/Feature/Common/Code/Source/Utils/EditorModelPreset.cpp

@@ -29,7 +29,6 @@ namespace AZ
                             ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                         ->DataElement(AZ::Edit::UIHandlers::Default, &ModelPreset::m_displayName, "Display Name", "Identifier used for display and selection")
                         ->DataElement(AZ::Edit::UIHandlers::Default, &ModelPreset::m_modelAsset, "Model Asset", "Model asset reference")
-                        ->DataElement(AZ::Edit::UIHandlers::Default, &ModelPreset::m_previewImageAsset, "Preview Image Asset", "Preview image asset reference")
                         ;
                 }
             }

+ 1 - 3
Gems/Atom/Feature/Common/Code/Source/Utils/ModelPreset.cpp

@@ -24,10 +24,9 @@ namespace AZ
             if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
             {
                 serializeContext->Class<ModelPreset>()
-                    ->Version(3)
+                    ->Version(4)
                     ->Field("displayName", &ModelPreset::m_displayName)
                     ->Field("modelAsset", &ModelPreset::m_modelAsset)
-                    ->Field("previewImageAsset", &ModelPreset::m_previewImageAsset)
                     ;
             }
 
@@ -41,7 +40,6 @@ namespace AZ
                     ->Constructor<const ModelPreset&>()
                     ->Property("displayName", BehaviorValueProperty(&ModelPreset::m_displayName))
                     ->Property("modelAsset", BehaviorValueProperty(&ModelPreset::m_modelAsset))
-                    ->Property("previewImageAsset", BehaviorValueProperty(&ModelPreset::m_previewImageAsset))
                     ;
             }
         }

+ 3 - 0
Gems/Atom/Tools/AtomToolsFramework/Code/CMakeLists.txt

@@ -42,6 +42,7 @@ ly_add_target(
             Gem::Atom_RHI.Reflect
             Gem::Atom_Feature_Common.Static
             Gem::Atom_Bootstrap.Headers
+            Gem::ImageProcessingAtom.Headers
 )
 
 ly_add_target(
@@ -60,6 +61,8 @@ ly_add_target(
     BUILD_DEPENDENCIES
         PRIVATE
             Gem::AtomToolsFramework.Static
+    RUNTIME_DEPENDENCIES
+        Gem::ImageProcessingAtom.Editor
 )
 
 ################################################################################

+ 16 - 2
Gems/Atom/Tools/AtomToolsFramework/Code/Include/AtomToolsFramework/Util/Util.h

@@ -8,8 +8,9 @@
 
 #pragma once
 
-#include <AzCore/PlatformDef.h>
 #include <AzCore/Asset/AssetManager.h>
+#include <AzCore/PlatformDef.h>
+#include <AzCore/Settings/SettingsRegistry.h>
 #include <AzCore/std/containers/vector.h>
 
 AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
@@ -18,11 +19,24 @@ AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnin
 #include <QStringList>
 AZ_POP_DISABLE_WARNING
 
+class QImage;
+
 namespace AtomToolsFramework
 {
+    template<typename T>
+    T GetSettingOrDefault(AZStd::string_view path, const T& defaultValue)
+    {
+        T result;
+        auto settingsRegistry = AZ::SettingsRegistry::Get();
+        return (settingsRegistry && settingsRegistry->Get(result, path)) ? result : defaultValue;
+    }
+
+    using LoadImageAsyncCallback = AZStd::function<void(const QImage&)>;
+    void LoadImageAsync(const AZStd::string& path, LoadImageAsyncCallback callback);
+
     QFileInfo GetSaveFileInfo(const QString& initialPath);
     QFileInfo GetOpenFileInfo(const AZStd::vector<AZ::Data::AssetType>& assetTypes);
     QFileInfo GetUniqueFileInfo(const QString& initialPath);
     QFileInfo GetDuplicationFileInfo(const QString& initialPath);
     bool LaunchTool(const QString& baseName, const QString& extension, const QStringList& arguments);
-}
+} // namespace AtomToolsFramework

+ 34 - 1
Gems/Atom/Tools/AtomToolsFramework/Code/Source/Util/Util.cpp

@@ -6,15 +6,18 @@
  *
  */
 
+#include <Atom/ImageProcessing/ImageObject.h>
+#include <Atom/ImageProcessing/ImageProcessingBus.h>
 #include <AtomToolsFramework/Util/Util.h>
 #include <AzCore/IO/SystemFile.h>
+#include <AzCore/Jobs/JobFunction.h>
 #include <AzCore/StringFunc/StringFunc.h>
 #include <AzCore/Utils/Utils.h>
 #include <AzFramework/API/ApplicationAPI.h>
 #include <AzQtComponents/Components/Widgets/FileDialog.h>
+#include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
 #include <AzToolsFramework/AssetBrowser/AssetBrowserEntry.h>
 #include <AzToolsFramework/AssetBrowser/AssetSelectionModel.h>
-#include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
 
 AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
 #include <QApplication>
@@ -24,6 +27,36 @@ AZ_POP_DISABLE_WARNING
 
 namespace AtomToolsFramework
 {
+    void LoadImageAsync(const AZStd::string& path, LoadImageAsyncCallback callback)
+    {
+        AZ::Job* job = AZ::CreateJobFunction(
+            [path, callback]()
+            {
+                ImageProcessingAtom::IImageObjectPtr imageObject;
+                ImageProcessingAtom::ImageProcessingRequestBus::BroadcastResult(
+                    imageObject, &ImageProcessingAtom::ImageProcessingRequests::LoadImagePreview, path);
+
+                if (imageObject)
+                {
+                    AZ::u8* imageBuf = nullptr;
+                    AZ::u32 pitch = 0;
+                    AZ::u32 mip = 0;
+                    imageObject->GetImagePointer(mip, imageBuf, pitch);
+                    const AZ::u32 width = imageObject->GetWidth(mip);
+                    const AZ::u32 height = imageObject->GetHeight(mip);
+
+                    QImage image(imageBuf, width, height, pitch, QImage::Format_RGBA8888);
+
+                    if (callback)
+                    {
+                        callback(image);
+                    }
+                }
+            },
+            true);
+        job->Start();
+    }
+
     QFileInfo GetSaveFileInfo(const QString& initialPath)
     {
         const QFileInfo initialFileInfo(initialPath);

+ 0 - 2
Gems/Atom/Tools/MaterialEditor/Code/CMakeLists.txt

@@ -60,7 +60,6 @@ ly_add_target(
             Gem::AtomToolsFramework.Editor
             Gem::Atom_RPI.Public
             Gem::Atom_Feature_Common.Public
-            Gem::ImageProcessingAtom.Headers
 )
 
 ly_add_target(
@@ -113,7 +112,6 @@ ly_add_target(
     RUNTIME_DEPENDENCIES
         Gem::AtomToolsFramework.Editor
         Gem::EditorPythonBindings.Editor
-        Gem::ImageProcessingAtom.Editor
 )
 
 ly_set_gem_variant_to_load(TARGETS MaterialEditor VARIANTS Tools)

+ 0 - 18
Gems/Atom/Tools/MaterialEditor/Code/Include/Atom/Viewport/MaterialViewportRequestBus.h

@@ -62,15 +62,6 @@ namespace MaterialEditor
         //! Get set of lighting preset names
         virtual MaterialViewportPresetNameSet GetLightingPresetNames() const = 0;
 
-        //! Set lighting preset preview image
-        //! @param preset used to set preview image
-        //! @param preview image
-        virtual void SetLightingPresetPreview(AZ::Render::LightingPresetPtr preset, const QImage& image) = 0;
-
-        //! Get lighting preset preview image
-        //! @param preset used to find preview image
-        virtual QImage GetLightingPresetPreview(AZ::Render::LightingPresetPtr preset) const = 0;
-
         //! Get model preset last save path
         //! @param preset to lookup last save path
         virtual AZStd::string GetLightingPresetLastSavePath(AZ::Render::LightingPresetPtr preset) const = 0;
@@ -108,15 +99,6 @@ namespace MaterialEditor
         //! Get set of model preset names
         virtual MaterialViewportPresetNameSet GetModelPresetNames() const = 0;
 
-        //! Set model preset preview image
-        //! @param preset used to set preview image
-        //! @param preview image
-        virtual void SetModelPresetPreview(AZ::Render::ModelPresetPtr preset, const QImage& image) = 0;
-
-        //! Get model preset preview image
-        //! @param preset used to find preview image
-        virtual QImage GetModelPresetPreview(AZ::Render::ModelPresetPtr preset) const = 0;
-
         //! Get model preset last save path
         //! @param preset to lookup last save path
         virtual AZStd::string GetModelPresetLastSavePath(AZ::Render::ModelPresetPtr preset) const = 0;

+ 10 - 91
Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/MaterialViewportComponent.cpp

@@ -6,61 +6,26 @@
  *
  */
 
+#include <Atom/RPI.Edit/Common/AssetUtils.h>
+#include <Atom/RPI.Edit/Common/JsonUtils.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
+#include <Atom/RPI.Reflect/System/AnyAsset.h>
+#include <Atom/Viewport/MaterialViewportNotificationBus.h>
+#include <Atom/Viewport/MaterialViewportSettings.h>
 #include <AzCore/Component/TickBus.h>
-#include <AzCore/Serialization/SerializeContext.h>
-#include <AzCore/Serialization/EditContext.h>
 #include <AzCore/RTTI/BehaviorContext.h>
-#include <AzCore/Jobs/JobFunction.h>
-
+#include <AzCore/Serialization/EditContext.h>
+#include <AzCore/Serialization/Json/JsonUtils.h>
+#include <AzCore/Serialization/SerializeContext.h>
 #include <AzFramework/Asset/AssetSystemBus.h>
-#include <AzFramework/StringFunc/StringFunc.h>
 #include <AzFramework/IO/LocalFileIO.h>
+#include <AzFramework/StringFunc/StringFunc.h>
 #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
 #include <AzToolsFramework/AssetBrowser/AssetBrowserEntry.h>
-
 #include <Viewport/MaterialViewportComponent.h>
-#include <Atom/Viewport/MaterialViewportNotificationBus.h>
-#include <Atom/Viewport/MaterialViewportSettings.h>
-
-#include <Atom/ImageProcessing/ImageObject.h>
-#include <Atom/ImageProcessing/ImageProcessingBus.h>
-
-#include <AzCore/Serialization/Json/JsonUtils.h>
-
-#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
-#include <Atom/RPI.Reflect/System/AnyAsset.h>
-#include <Atom/RPI.Edit/Common/AssetUtils.h>
-#include <Atom/RPI.Edit/Common/JsonUtils.h>
 
 namespace MaterialEditor
 {
-    using LoadImageAsyncCallback = AZStd::function<void(const QImage&)>;
-    void LoadImageAsync(const AZStd::string& path, LoadImageAsyncCallback callback)
-    {
-        AZ::Job* job = AZ::CreateJobFunction([path, callback]() {
-            ImageProcessingAtom::IImageObjectPtr imageObject;
-            ImageProcessingAtom::ImageProcessingRequestBus::BroadcastResult(imageObject, &ImageProcessingAtom::ImageProcessingRequests::LoadImagePreview, path);
-
-            if (imageObject)
-            {
-                AZ::u8* imageBuf = nullptr;
-                AZ::u32 pitch = 0;
-                AZ::u32 mip = 0;
-                imageObject->GetImagePointer(mip, imageBuf, pitch);
-                const AZ::u32 width = imageObject->GetWidth(mip);
-                const AZ::u32 height = imageObject->GetHeight(mip);
-
-                QImage image(imageBuf, width, height, pitch, QImage::Format_RGBA8888);
-
-                if (callback)
-                {
-                    callback(image);
-                }
-            }
-            }, true);
-        job->Start();
-    }
-
     MaterialViewportComponent::MaterialViewportComponent()
     {
     }
@@ -162,12 +127,6 @@ namespace MaterialEditor
         m_viewportSettings =
             AZ::UserSettings::CreateFind<MaterialViewportSettings>(AZ::Crc32("MaterialViewportSettings"), AZ::UserSettings::CT_GLOBAL);
 
-        m_lightingPresetPreviewImageDefault = QImage(180, 90, QImage::Format::Format_RGBA8888);
-        m_lightingPresetPreviewImageDefault.fill(Qt::GlobalColor::black);
-
-        m_modelPresetPreviewImageDefault = QImage(90, 90, QImage::Format::Format_RGBA8888);
-        m_modelPresetPreviewImageDefault.fill(Qt::GlobalColor::black);
-
         MaterialViewportRequestBus::Handler::BusConnect();
         AzFramework::AssetCatalogEventBus::Handler::BusConnect();
     }
@@ -177,12 +136,10 @@ namespace MaterialEditor
         AzFramework::AssetCatalogEventBus::Handler::BusDisconnect();
         MaterialViewportRequestBus::Handler::BusDisconnect();
 
-        m_lightingPresetPreviewImages.clear();
         m_lightingPresetVector.clear();
         m_lightingPresetLastSavePathMap.clear();
         m_lightingPresetSelection.reset();
 
-        m_modelPresetPreviewImages.clear();
         m_modelPresetVector.clear();
         m_modelPresetLastSavePathMap.clear();
         m_modelPresetSelection.reset();
@@ -286,14 +243,6 @@ namespace MaterialEditor
             SelectLightingPreset(presetPtr);
         }
 
-        const auto& imagePath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(presetPtr->m_skyboxImageAsset.GetId());
-        LoadImageAsync(imagePath, [presetPtr](const QImage& image) {
-            QImage imageScaled = image.scaled(180, 90, Qt::AspectRatioMode::KeepAspectRatio);
-            AZ::TickBus::QueueFunction([presetPtr, imageScaled]() {
-                MaterialViewportRequestBus::Broadcast(&MaterialViewportRequestBus::Events::SetLightingPresetPreview, presetPtr, imageScaled);
-                });
-            });
-
         return presetPtr;
     }
 
@@ -353,17 +302,6 @@ namespace MaterialEditor
         return names;
     }
 
-    void MaterialViewportComponent::SetLightingPresetPreview(AZ::Render::LightingPresetPtr preset, const QImage& image)
-    {
-        m_lightingPresetPreviewImages[preset] = image;
-    }
-
-    QImage MaterialViewportComponent::GetLightingPresetPreview(AZ::Render::LightingPresetPtr preset) const
-    {
-        auto imageItr = m_lightingPresetPreviewImages.find(preset);
-        return imageItr != m_lightingPresetPreviewImages.end() ? imageItr->second : m_lightingPresetPreviewImageDefault;
-    }
-
     AZStd::string MaterialViewportComponent::GetLightingPresetLastSavePath(AZ::Render::LightingPresetPtr preset) const
     {
         auto pathItr = m_lightingPresetLastSavePathMap.find(preset);
@@ -382,14 +320,6 @@ namespace MaterialEditor
             SelectModelPreset(presetPtr);
         }
 
-        const auto& imagePath = AZ::RPI::AssetUtils::GetSourcePathByAssetId(presetPtr->m_previewImageAsset.GetId());
-        LoadImageAsync(imagePath, [presetPtr](const QImage& image) {
-            QImage imageScaled = image.scaled(90, 90, Qt::AspectRatioMode::KeepAspectRatio);
-            AZ::TickBus::QueueFunction([presetPtr, imageScaled]() {
-                MaterialViewportRequestBus::Broadcast(&MaterialViewportRequestBus::Events::SetModelPresetPreview, presetPtr, imageScaled);
-                });
-            });
-
         return presetPtr;
     }
 
@@ -449,17 +379,6 @@ namespace MaterialEditor
         return names;
     }
 
-    void MaterialViewportComponent::SetModelPresetPreview(AZ::Render::ModelPresetPtr preset, const QImage& image)
-    {
-        m_modelPresetPreviewImages[preset] = image;
-    }
-
-    QImage MaterialViewportComponent::GetModelPresetPreview(AZ::Render::ModelPresetPtr preset) const
-    {
-        auto imageItr = m_modelPresetPreviewImages.find(preset);
-        return imageItr != m_modelPresetPreviewImages.end() ? imageItr->second : m_modelPresetPreviewImageDefault;
-    }
-
     AZStd::string MaterialViewportComponent::GetModelPresetLastSavePath(AZ::Render::ModelPresetPtr preset) const
     {
         auto pathItr = m_modelPresetLastSavePathMap.find(preset);

+ 0 - 10
Gems/Atom/Tools/MaterialEditor/Code/Source/Viewport/MaterialViewportComponent.h

@@ -58,8 +58,6 @@ namespace MaterialEditor
         void SelectLightingPreset(AZ::Render::LightingPresetPtr preset) override;
         void SelectLightingPresetByName(const AZStd::string& name) override;
         MaterialViewportPresetNameSet GetLightingPresetNames() const override;
-        void SetLightingPresetPreview(AZ::Render::LightingPresetPtr preset, const QImage& image) override;
-        QImage GetLightingPresetPreview(AZ::Render::LightingPresetPtr preset) const override;
         AZStd::string GetLightingPresetLastSavePath(AZ::Render::LightingPresetPtr preset) const override;
 
         AZ::Render::ModelPresetPtr AddModelPreset(const AZ::Render::ModelPreset& preset) override;
@@ -70,8 +68,6 @@ namespace MaterialEditor
         void SelectModelPreset(AZ::Render::ModelPresetPtr preset) override;
         void SelectModelPresetByName(const AZStd::string& name) override;
         MaterialViewportPresetNameSet GetModelPresetNames() const override;
-        void SetModelPresetPreview(AZ::Render::ModelPresetPtr preset, const QImage& image) override;
-        QImage GetModelPresetPreview(AZ::Render::ModelPresetPtr preset) const override;
         AZStd::string GetModelPresetLastSavePath(AZ::Render::ModelPresetPtr preset) const override;
 
         void SetShadowCatcherEnabled(bool enable) override;
@@ -97,12 +93,6 @@ namespace MaterialEditor
         AZ::Render::ModelPresetPtrVector m_modelPresetVector;
         AZ::Render::ModelPresetPtr m_modelPresetSelection;
 
-        AZStd::map<AZ::Render::LightingPresetPtr, QImage> m_lightingPresetPreviewImages;
-        AZStd::map<AZ::Render::ModelPresetPtr, QImage> m_modelPresetPreviewImages;
-
-        QImage m_lightingPresetPreviewImageDefault;
-        QImage m_modelPresetPreviewImageDefault;
-
         mutable AZStd::map<AZ::Render::LightingPresetPtr, AZStd::string> m_lightingPresetLastSavePathMap;
         mutable AZStd::map<AZ::Render::ModelPresetPtr, AZStd::string> m_modelPresetLastSavePathMap;
 

+ 8 - 4
Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/LightingPresetBrowserDialog.cpp

@@ -7,6 +7,7 @@
  */
 
 #include <Atom/Feature/Utils/LightingPreset.h>
+#include <Atom/RPI.Edit/Common/AssetUtils.h>
 #include <Atom/Viewport/MaterialViewportRequestBus.h>
 #include <AtomToolsFramework/Util/Util.h>
 #include <AzFramework/Application/Application.h>
@@ -27,13 +28,16 @@ namespace MaterialEditor
         MaterialViewportRequestBus::BroadcastResult(presets, &MaterialViewportRequestBus::Events::GetLightingPresets);
         AZStd::sort(presets.begin(), presets.end(), [](const auto& a, const auto& b) { return a->m_displayName < b->m_displayName; });
 
+        const int itemSize = aznumeric_cast<int>(
+            AtomToolsFramework::GetSettingOrDefault<AZ::u64>("/O3DE/Atom/MaterialEditor/PresetBrowserDialog/LightingItemSize", 180));
+
         QListWidgetItem* selectedItem = nullptr;
         for (const auto& preset : presets)
         {
-            QImage image;
-            MaterialViewportRequestBus::BroadcastResult(image, &MaterialViewportRequestBus::Events::GetLightingPresetPreview, preset);
-
-            QListWidgetItem* item = CreateListItem(preset->m_displayName.c_str(), image);
+            AZStd::string path;
+            MaterialViewportRequestBus::BroadcastResult(path, &MaterialViewportRequestBus::Events::GetLightingPresetLastSavePath, preset);
+            QListWidgetItem* item = CreateListItem(
+                preset->m_displayName.c_str(), AZ::RPI::AssetUtils::MakeAssetId(path, 0).GetValue(), QSize(itemSize, itemSize));
 
             m_listItemToPresetMap[item] = preset;
 

+ 4 - 4
Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/ModelPresetBrowserDialog.cpp

@@ -27,13 +27,13 @@ namespace MaterialEditor
         MaterialViewportRequestBus::BroadcastResult(presets, &MaterialViewportRequestBus::Events::GetModelPresets);
         AZStd::sort(presets.begin(), presets.end(), [](const auto& a, const auto& b) { return a->m_displayName < b->m_displayName; });
 
+        const int itemSize = aznumeric_cast<int>(
+            AtomToolsFramework::GetSettingOrDefault<AZ::u64>("/O3DE/Atom/MaterialEditor/PresetBrowserDialog/ModelItemSize", 90));
+
         QListWidgetItem* selectedItem = nullptr;
         for (const auto& preset : presets)
         {
-            QImage image;
-            MaterialViewportRequestBus::BroadcastResult(image, &MaterialViewportRequestBus::Events::GetModelPresetPreview, preset);
-
-            QListWidgetItem* item = CreateListItem(preset->m_displayName.c_str(), image);
+            QListWidgetItem* item = CreateListItem(preset->m_displayName.c_str(), preset->m_modelAsset.GetId(), QSize(itemSize, itemSize));
 
             m_listItemToPresetMap[item] = preset;
 

+ 44 - 23
Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/PresetBrowserDialog.cpp

@@ -12,11 +12,16 @@
 #include <AzQtComponents/Components/Widgets/ElidingLabel.h>
 #include <AzQtComponents/Components/Widgets/LineEdit.h>
 #include <AzQtComponents/Components/Widgets/Text.h>
+#include <AzToolsFramework/AssetBrowser/Thumbnails/ProductThumbnail.h>
+#include <AzToolsFramework/Thumbnails/ThumbnailContext.h>
+#include <AzToolsFramework/Thumbnails/ThumbnailWidget.h>
+#include <AzToolsFramework/Thumbnails/ThumbnailerBus.h>
 #include <Window/PresetBrowserDialogs/PresetBrowserDialog.h>
 
 #include <QLabel>
 #include <QLineEdit>
 #include <QMenu>
+#include <QVBoxLayout>
 
 namespace MaterialEditor
 {
@@ -41,35 +46,51 @@ namespace MaterialEditor
         m_ui->m_presetList->setGridSize(QSize(0, 0));
         m_ui->m_presetList->setWrapping(true);
 
-        QObject::connect(m_ui->m_presetList, &QListWidget::currentItemChanged, [this]() { SelectCurrentPreset(); });
+        QObject::connect(m_ui->m_presetList, &QListWidget::currentItemChanged, [this](){ SelectCurrentPreset(); });
     }
 
-    QListWidgetItem* PresetBrowserDialog::CreateListItem(const QString& title, const QImage& image)
+    QListWidgetItem* PresetBrowserDialog::CreateListItem(const QString& title, const AZ::Data::AssetId& assetId, const QSize& size)
     {
+        const int itemBorder = aznumeric_cast<int>(
+            AtomToolsFramework::GetSettingOrDefault<AZ::u64>("/O3DE/Atom/MaterialEditor/PresetBrowserDialog/ItemBorder", 4));
+        const int itemSpacing = aznumeric_cast<int>(
+            AtomToolsFramework::GetSettingOrDefault<AZ::u64>("/O3DE/Atom/MaterialEditor/PresetBrowserDialog/ItemSpacing", 10));
+        const int headerHeight = aznumeric_cast<int>(
+            AtomToolsFramework::GetSettingOrDefault<AZ::u64>("/O3DE/Atom/MaterialEditor/PresetBrowserDialog/HeaderHeight", 15));
+
         const QSize gridSize = m_ui->m_presetList->gridSize();
-        m_ui->m_presetList->setGridSize(
-            QSize(AZStd::max(gridSize.width(), image.width() + 10), AZStd::max(gridSize.height(), image.height() + 10)));
+        m_ui->m_presetList->setGridSize(QSize(
+            AZStd::max(gridSize.width(), size.width() + itemSpacing),
+            AZStd::max(gridSize.height(), size.height() + itemSpacing + headerHeight)));
 
         QListWidgetItem* item = new QListWidgetItem(m_ui->m_presetList);
         item->setData(Qt::UserRole, title);
-        item->setSizeHint(image.size() + QSize(4, 4));
+        item->setSizeHint(size + QSize(itemBorder, itemBorder + headerHeight));
         m_ui->m_presetList->addItem(item);
 
-        QLabel* previewImage = new QLabel(m_ui->m_presetList);
-        previewImage->setFixedSize(image.size());
-        previewImage->setMargin(0);
-        previewImage->setPixmap(QPixmap::fromImage(image));
-        previewImage->updateGeometry();
-
-        AzQtComponents::ElidingLabel* previewLabel = new AzQtComponents::ElidingLabel(previewImage);
-        previewLabel->setText(title);
-        previewLabel->setFixedSize(QSize(image.width(), 15));
-        previewLabel->setMargin(0);
-        previewLabel->setStyleSheet("background-color: rgb(35, 35, 35)");
-        AzQtComponents::Text::addPrimaryStyle(previewLabel);
-        AzQtComponents::Text::addLabelStyle(previewLabel);
-
-        m_ui->m_presetList->setItemWidget(item, previewImage);
+        QWidget* itemWidget = new QWidget(m_ui->m_presetList);
+        itemWidget->setLayout(new QVBoxLayout(itemWidget));
+        itemWidget->layout()->setSpacing(0);
+        itemWidget->layout()->setMargin(0);
+
+        AzQtComponents::ElidingLabel* header = new AzQtComponents::ElidingLabel(itemWidget);
+        header->setText(title);
+        header->setFixedSize(QSize(size.width(), headerHeight));
+        header->setMargin(0);
+        header->setStyleSheet("background-color: rgb(35, 35, 35)");
+        AzQtComponents::Text::addPrimaryStyle(header);
+        AzQtComponents::Text::addLabelStyle(header);
+        itemWidget->layout()->addWidget(header);
+
+        AzToolsFramework::Thumbnailer::ThumbnailWidget* thumbnail = new AzToolsFramework::Thumbnailer::ThumbnailWidget(itemWidget);
+        thumbnail->setFixedSize(size);
+        thumbnail->SetThumbnailKey(
+            MAKE_TKEY(AzToolsFramework::AssetBrowser::ProductThumbnailKey, assetId),
+            AzToolsFramework::Thumbnailer::ThumbnailContext::DefaultContext);
+        thumbnail->updateGeometry();
+        itemWidget->layout()->addWidget(thumbnail);
+
+        m_ui->m_presetList->setItemWidget(item, itemWidget);
 
         return item;
     }
@@ -79,15 +100,15 @@ namespace MaterialEditor
         m_ui->m_searchWidget->setReadOnly(false);
         m_ui->m_searchWidget->setContextMenuPolicy(Qt::CustomContextMenu);
         AzQtComponents::LineEdit::applySearchStyle(m_ui->m_searchWidget);
-        connect(m_ui->m_searchWidget, &QLineEdit::textChanged, this, [this]() { ApplySearchFilter(); });
-        connect(m_ui->m_searchWidget, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { ShowSearchMenu(pos); });
+        connect(m_ui->m_searchWidget, &QLineEdit::textChanged, this, [this](){ ApplySearchFilter(); });
+        connect(m_ui->m_searchWidget, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos){ ShowSearchMenu(pos); });
     }
 
     void PresetBrowserDialog::SetupDialogButtons()
     {
         connect(m_ui->m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
         connect(m_ui->m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
-        connect(this, &QDialog::rejected, this, [this]() { SelectInitialPreset(); });
+        connect(this, &QDialog::rejected, this, [this](){ SelectInitialPreset(); });
     }
 
     void PresetBrowserDialog::ApplySearchFilter()

+ 3 - 3
Gems/Atom/Tools/MaterialEditor/Code/Source/Window/PresetBrowserDialogs/PresetBrowserDialog.h

@@ -9,12 +9,12 @@
 #pragma once
 
 #if !defined(Q_MOC_RUN)
+#include <AzCore/Asset/AssetCommon.h>
 #include <AzCore/std/containers/vector.h>
-
 #include <QDialog>
 #endif
 
-#include <Source/Window/PresetBrowserDialogs/ui_PresetBrowserDialog.h>
+#include <Window/PresetBrowserDialogs/ui_PresetBrowserDialog.h>
 
 class QImage;
 class QListWidgetItem;
@@ -32,7 +32,7 @@ namespace MaterialEditor
 
 protected:
         void SetupPresetList();
-        QListWidgetItem* CreateListItem(const QString& title, const QImage& image);
+        QListWidgetItem* CreateListItem(const QString& title, const AZ::Data::AssetId& assetId, const QSize& size);
 
         void SetupSearchWidget();
         void SetupDialogButtons();

+ 3 - 3
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewContent.cpp

@@ -163,9 +163,9 @@ namespace AZ
                 m_modelAsset->GetAabb().GetAsSphere(center, radius);
             }
 
-            const auto distance = radius + NearDist;
-            const auto cameraRotation = Quaternion::CreateFromAxisAngle(Vector3::CreateAxisZ(), CameraRotationAngle);
-            const auto cameraPosition = center + cameraRotation.TransformVector(Vector3(0.0f, distance, 0.0f));
+            const auto distance = fabsf(radius / sinf(FieldOfView)) + NearDist;
+            const auto cameraRotation = Quaternion::CreateFromAxisAngle(Vector3::CreateAxisX(), -CameraRotationAngle);
+            const auto cameraPosition = center + cameraRotation.TransformVector(-Vector3::CreateAxisY() * distance);
             const auto cameraTransform = Transform::CreateLookAt(cameraPosition, center);
             m_view->SetCameraTransform(Matrix3x4::CreateFromTransform(cameraTransform));
         }

+ 1 - 1
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewContent.h

@@ -51,7 +51,7 @@ namespace AZ
             static constexpr float NearDist = 0.001f;
             static constexpr float FarDist = 100.0f;
             static constexpr float FieldOfView = Constants::HalfPi;
-            static constexpr float CameraRotationAngle = Constants::QuarterPi / 2.0f;
+            static constexpr float CameraRotationAngle = Constants::QuarterPi / 3.0f;
 
             RPI::ScenePtr m_scene;
             RPI::ViewPtr m_view;

+ 42 - 47
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewUtils.cpp

@@ -9,9 +9,11 @@
 #include <API/EditorAssetSystemAPI.h>
 #include <AssetBrowser/Thumbnails/ProductThumbnail.h>
 #include <AssetBrowser/Thumbnails/SourceThumbnail.h>
+#include <Atom/RPI.Reflect/Asset/AssetUtils.h>
 #include <Atom/RPI.Reflect/Material/MaterialAsset.h>
 #include <Atom/RPI.Reflect/Model/ModelAsset.h>
 #include <Atom/RPI.Reflect/System/AnyAsset.h>
+#include <AtomToolsFramework/Util/Util.h>
 #include <SharedPreview/SharedPreviewUtils.h>
 
 namespace AZ
@@ -20,45 +22,64 @@ namespace AZ
     {
         namespace SharedPreviewUtils
         {
-            Data::AssetId GetAssetId(
-                AzToolsFramework::Thumbnailer::SharedThumbnailKey key,
-                const Data::AssetType& assetType,
-                const Data::AssetId& defaultAssetId)
+            AZStd::unordered_set<AZ::Uuid> GetSupportedAssetTypes()
+            {
+                return { RPI::AnyAsset::RTTI_Type(), RPI::MaterialAsset::RTTI_Type(), RPI::ModelAsset::RTTI_Type() };
+            }
+
+            bool IsSupportedAssetType(AzToolsFramework::Thumbnailer::SharedThumbnailKey key)
             {
+                return GetSupportedAssetInfo(key).m_assetId.IsValid();
+            }
+
+            AZ::Data::AssetInfo GetSupportedAssetInfo(AzToolsFramework::Thumbnailer::SharedThumbnailKey key)
+            {
+                const auto& supportedTypeIds = GetSupportedAssetTypes();
+
                 // if it's a source thumbnail key, find first product with a matching asset type
                 auto sourceKey = azrtti_cast<const AzToolsFramework::AssetBrowser::SourceThumbnailKey*>(key.data());
                 if (sourceKey)
                 {
                     bool foundIt = false;
-                    AZStd::vector<Data::AssetInfo> productsAssetInfo;
+                    AZStd::vector<AZ::Data::AssetInfo> productsAssetInfo;
                     AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
                         foundIt, &AzToolsFramework::AssetSystemRequestBus::Events::GetAssetsProducedBySourceUUID,
                         sourceKey->GetSourceUuid(), productsAssetInfo);
-                    if (!foundIt)
+
+                    for (const auto& assetInfo : productsAssetInfo)
                     {
-                        return defaultAssetId;
-                    }
-                    auto assetInfoIt = AZStd::find_if(
-                        productsAssetInfo.begin(), productsAssetInfo.end(),
-                        [&assetType](const Data::AssetInfo& assetInfo)
+                        if (supportedTypeIds.find(assetInfo.m_assetType) != supportedTypeIds.end())
                         {
-                            return assetInfo.m_assetType == assetType;
-                        });
-                    if (assetInfoIt == productsAssetInfo.end())
-                    {
-                        return defaultAssetId;
+                            return assetInfo;
+                        }
                     }
-
-                    return assetInfoIt->m_assetId;
+                    return AZ::Data::AssetInfo();
                 }
 
                 // if it's a product thumbnail key just return its assetId
+                AZ::Data::AssetInfo assetInfo;
                 auto productKey = azrtti_cast<const AzToolsFramework::AssetBrowser::ProductThumbnailKey*>(key.data());
-                if (productKey && productKey->GetAssetType() == assetType)
+                if (productKey && supportedTypeIds.find(productKey->GetAssetType()) != supportedTypeIds.end())
                 {
-                    return productKey->GetAssetId();
+                    AZ::Data::AssetCatalogRequestBus::BroadcastResult(
+                        assetInfo, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetInfoById, productKey->GetAssetId());
                 }
-                return defaultAssetId;
+                return assetInfo;
+            }
+
+            AZ::Data::AssetId GetSupportedAssetId(AzToolsFramework::Thumbnailer::SharedThumbnailKey key, const AZ::Data::AssetId& defaultAssetId)
+            {
+                const AZ::Data::AssetInfo assetInfo = GetSupportedAssetInfo(key);
+                return assetInfo.m_assetId.IsValid() ? assetInfo.m_assetId : defaultAssetId;
+            }
+
+            AZ::Data::AssetId GetAssetIdForProductPath(const AZStd::string_view productPath)
+            {
+                if (!productPath.empty())
+                {
+                    return AZ::RPI::AssetUtils::GetAssetIdForProductPath(productPath.data());
+                }
+                return AZ::Data::AssetId();
             }
 
             QString WordWrap(const QString& string, int maxLength)
@@ -85,32 +106,6 @@ namespace AZ
                 }
                 return result;
             }
-
-            AZStd::unordered_set<AZ::Uuid> GetSupportedAssetTypes()
-            {
-                return { RPI::AnyAsset::RTTI_Type(), RPI::MaterialAsset::RTTI_Type(), RPI::ModelAsset::RTTI_Type() };
-            }
-
-            bool IsSupportedAssetType(AzToolsFramework::Thumbnailer::SharedThumbnailKey key)
-            {
-                for (const AZ::Uuid& typeId : SharedPreviewUtils::GetSupportedAssetTypes())
-                {
-                    const AZ::Data::AssetId& assetId = SharedPreviewUtils::GetAssetId(key, typeId);
-                    if (assetId.IsValid())
-                    {
-                        if (typeId == RPI::AnyAsset::RTTI_Type())
-                        {
-                            AZ::Data::AssetInfo assetInfo;
-                            AZ::Data::AssetCatalogRequestBus::BroadcastResult(
-                                assetInfo, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetInfoById, assetId);
-                            return AzFramework::StringFunc::EndsWith(assetInfo.m_relativePath.c_str(), "lightingpreset.azasset");
-                        }
-                        return true;
-                    }
-                }
-
-                return false;
-            }
         } // namespace SharedPreviewUtils
     } // namespace LyIntegration
 } // namespace AZ

+ 16 - 12
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedPreviewUtils.h

@@ -8,9 +8,9 @@
 
 #pragma once
 
-#include <AzCore/Asset/AssetCommon.h>
-
 #if !defined(Q_MOC_RUN)
+#include <AzCore/Asset/AssetCommon.h>
+#include <AzCore/Asset/AssetManagerBus.h>
 #include <AzToolsFramework/Thumbnails/Thumbnail.h>
 #endif
 
@@ -20,21 +20,25 @@ namespace AZ
     {
         namespace SharedPreviewUtils
         {
-            //! Get assetId by assetType that belongs to either source or product thumbnail key
-            Data::AssetId GetAssetId(
-                AzToolsFramework::Thumbnailer::SharedThumbnailKey key,
-                const Data::AssetType& assetType,
-                const Data::AssetId& defaultAssetId = {});
-
-            //! Word wrap function for previewer QLabel, since by default it does not break long words such as filenames, so manual word
-            //! wrap needed
-            QString WordWrap(const QString& string, int maxLength);
-
             //! Get the set of all asset types supported by the shared preview
             AZStd::unordered_set<AZ::Uuid> GetSupportedAssetTypes();
 
             //! Determine if a thumbnail key has an asset supported by the shared preview
             bool IsSupportedAssetType(AzToolsFramework::Thumbnailer::SharedThumbnailKey key);
+
+            //! Get assetInfo of source or product thumbnail key if asset type is supported by the shared preview
+            AZ::Data::AssetInfo GetSupportedAssetInfo(AzToolsFramework::Thumbnailer::SharedThumbnailKey key);
+
+            //! Get assetId of source or product thumbnail key if asset type is supported by the shared preview
+            AZ::Data::AssetId GetSupportedAssetId(
+                AzToolsFramework::Thumbnailer::SharedThumbnailKey key, const AZ::Data::AssetId& defaultAssetId = {});
+
+            //! Wraps AZ::RPI::AssetUtils::GetAssetIdForProductPath to handle empty productPath
+            AZ::Data::AssetId GetAssetIdForProductPath(const AZStd::string_view productPath);
+
+            //! Inserts new line characters into a string whenever the maximum number of characters per line is exceeded
+            QString WordWrap(const QString& string, int maxLength);
+
         } // namespace SharedPreviewUtils
     } // namespace LyIntegration
 } // namespace AZ

+ 9 - 12
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnail.cpp

@@ -22,18 +22,13 @@ namespace AZ
         //////////////////////////////////////////////////////////////////////////
         SharedThumbnail::SharedThumbnail(AzToolsFramework::Thumbnailer::SharedThumbnailKey key)
             : Thumbnail(key)
+            , m_assetInfo(SharedPreviewUtils::GetSupportedAssetInfo(key))
         {
-            for (const AZ::Uuid& typeId : SharedPreviewUtils::GetSupportedAssetTypes())
+            if (m_assetInfo.m_assetId.IsValid())
             {
-                const AZ::Data::AssetId& assetId = SharedPreviewUtils::GetAssetId(key, typeId);
-                if (assetId.IsValid())
-                {
-                    m_assetId = assetId;
-                    m_typeId = typeId;
-                    AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Handler::BusConnect(key);
-                    AzFramework::AssetCatalogEventBus::Handler::BusConnect();
-                    return;
-                }
+                AzToolsFramework::Thumbnailer::ThumbnailerRendererNotificationBus::Handler::BusConnect(key);
+                AzFramework::AssetCatalogEventBus::Handler::BusConnect();
+                return;
             }
 
             AZ_Error("SharedThumbnail", false, "Failed to find matching assetId for the thumbnailKey.");
@@ -43,7 +38,9 @@ namespace AZ
         void SharedThumbnail::LoadThread()
         {
             AzToolsFramework::Thumbnailer::ThumbnailerRendererRequestBus::QueueEvent(
-                m_typeId, &AzToolsFramework::Thumbnailer::ThumbnailerRendererRequests::RenderThumbnail, m_key, SharedThumbnailSize);
+                m_assetInfo.m_assetType, &AzToolsFramework::Thumbnailer::ThumbnailerRendererRequests::RenderThumbnail, m_key,
+                SharedThumbnailSize);
+
             // wait for response from thumbnail renderer
             m_renderWait.acquire();
         }
@@ -68,7 +65,7 @@ namespace AZ
 
         void SharedThumbnail::OnCatalogAssetChanged([[maybe_unused]] const AZ::Data::AssetId& assetId)
         {
-            if (m_assetId == assetId && m_state == State::Ready)
+            if (m_assetInfo.m_assetId == assetId && m_state == State::Ready)
             {
                 m_state = State::Unloaded;
                 Load();

+ 1 - 2
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnail.h

@@ -43,8 +43,7 @@ namespace AZ
             void OnCatalogAssetChanged(const AZ::Data::AssetId& assetId) override;
 
             AZStd::binary_semaphore m_renderWait;
-            Data::AssetId m_assetId;
-            AZ::Uuid m_typeId;
+            Data::AssetInfo m_assetInfo;
         };
 
         //! Cache configuration for shared thumbnails

+ 56 - 6
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnailRenderer.cpp

@@ -8,6 +8,7 @@
 
 #include <AtomToolsFramework/PreviewRenderer/PreviewRendererCaptureRequest.h>
 #include <AtomToolsFramework/PreviewRenderer/PreviewRendererInterface.h>
+#include <AtomToolsFramework/Util/Util.h>
 #include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
 #include <AzToolsFramework/Thumbnails/ThumbnailerBus.h>
 #include <SharedPreview/SharedPreviewContent.h>
@@ -20,9 +21,9 @@ namespace AZ
     {
         SharedThumbnailRenderer::SharedThumbnailRenderer()
         {
-            m_defaultModelAsset.Create(DefaultModelAssetId, true);
-            m_defaultMaterialAsset.Create(DefaultMaterialAssetId, true);
-            m_defaultLightingPresetAsset.Create(DefaultLightingPresetAssetId, true);
+            m_defaultModelAsset.Create(SharedPreviewUtils::GetAssetIdForProductPath(DefaultModelPath), true);
+            m_defaultMaterialAsset.Create(SharedPreviewUtils::GetAssetIdForProductPath(DefaultMaterialPath), true);
+            m_defaultLightingPresetAsset.Create(SharedPreviewUtils::GetAssetIdForProductPath(DefaultLightingPresetPath), true);
 
             for (const AZ::Uuid& typeId : SharedPreviewUtils::GetSupportedAssetTypes())
             {
@@ -37,17 +38,66 @@ namespace AZ
             SystemTickBus::Handler::BusDisconnect();
         }
 
+        SharedThumbnailRenderer::ThumbnailConfig SharedThumbnailRenderer::GetThumbnailConfig(
+            AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey)
+        {
+            ThumbnailConfig thumbnailConfig;
+
+            const auto assetInfo = SharedPreviewUtils::GetSupportedAssetInfo(thumbnailKey);
+            if (assetInfo.m_assetType == RPI::ModelAsset::RTTI_Type())
+            {
+                static constexpr const char* MaterialAssetPathSetting =
+                    "/O3DE/Atom/CommonFeature/SharedPreview/ModelAssetType/MaterialAssetPath";
+                static constexpr const char* LightingAssetPathSetting =
+                    "/O3DE/Atom/CommonFeature/SharedPreview/ModelAssetType/LightingAssetPath";
+
+                thumbnailConfig.m_modelId = assetInfo.m_assetId;
+                thumbnailConfig.m_materialId = SharedPreviewUtils::GetAssetIdForProductPath(
+                    AtomToolsFramework::GetSettingOrDefault<AZStd::string>(MaterialAssetPathSetting, DefaultMaterialPath));
+                thumbnailConfig.m_lightingId = SharedPreviewUtils::GetAssetIdForProductPath(
+                    AtomToolsFramework::GetSettingOrDefault<AZStd::string>(LightingAssetPathSetting, DefaultLightingPresetPath));
+            }
+            else if (assetInfo.m_assetType == RPI::MaterialAsset::RTTI_Type())
+            {
+                static constexpr const char* ModelAssetPathSetting =
+                    "/O3DE/Atom/CommonFeature/SharedPreview/MaterialAssetType/ModelAssetPath";
+                static constexpr const char* LightingAssetPathSetting =
+                    "/O3DE/Atom/CommonFeature/SharedPreview/MaterialAssetType/LightingAssetPath";
+
+                thumbnailConfig.m_modelId = SharedPreviewUtils::GetAssetIdForProductPath(
+                    AtomToolsFramework::GetSettingOrDefault<AZStd::string>(ModelAssetPathSetting, DefaultModelPath));
+                thumbnailConfig.m_materialId = assetInfo.m_assetId;
+                thumbnailConfig.m_lightingId = SharedPreviewUtils::GetAssetIdForProductPath(
+                    AtomToolsFramework::GetSettingOrDefault<AZStd::string>(LightingAssetPathSetting, DefaultLightingPresetPath));
+            }
+            else if (assetInfo.m_assetType == RPI::AnyAsset::RTTI_Type())
+            {
+                static constexpr const char* ModelAssetPathSetting =
+                    "/O3DE/Atom/CommonFeature/SharedPreview/LightingAssetType/ModelAssetPath";
+                static constexpr const char* MaterialAssetPathSetting =
+                    "/O3DE/Atom/CommonFeature/SharedPreview/LightingAssetType/MaterialAssetPath";
+
+                thumbnailConfig.m_modelId = SharedPreviewUtils::GetAssetIdForProductPath(
+                    AtomToolsFramework::GetSettingOrDefault<AZStd::string>(ModelAssetPathSetting, DefaultModelPath));
+                thumbnailConfig.m_materialId = SharedPreviewUtils::GetAssetIdForProductPath(
+                    AtomToolsFramework::GetSettingOrDefault<AZStd::string>(MaterialAssetPathSetting, "materials/reflectionprobe/reflectionprobevisualization.azmaterial"));
+                thumbnailConfig.m_lightingId = assetInfo.m_assetId;
+            }
+
+            return thumbnailConfig;
+        }
+
         void SharedThumbnailRenderer::RenderThumbnail(AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize)
         {
             if (auto previewRenderer = AZ::Interface<AtomToolsFramework::PreviewRendererInterface>::Get())
             {
+                const auto& thumbnailConfig = GetThumbnailConfig(thumbnailKey);
+
                 previewRenderer->AddCaptureRequest(
                     { thumbnailSize,
                       AZStd::make_shared<SharedPreviewContent>(
                           previewRenderer->GetScene(), previewRenderer->GetView(), previewRenderer->GetEntityContextId(),
-                          SharedPreviewUtils::GetAssetId(thumbnailKey, RPI::ModelAsset::RTTI_Type(), DefaultModelAssetId),
-                          SharedPreviewUtils::GetAssetId(thumbnailKey, RPI::MaterialAsset::RTTI_Type(), DefaultMaterialAssetId),
-                          SharedPreviewUtils::GetAssetId(thumbnailKey, RPI::AnyAsset::RTTI_Type(), DefaultLightingPresetAssetId),
+                          thumbnailConfig.m_modelId, thumbnailConfig.m_materialId, thumbnailConfig.m_lightingId,
                           Render::MaterialPropertyOverrideMap()),
                       [thumbnailKey]()
                       {

+ 9 - 3
Gems/AtomLyIntegration/CommonFeatures/Code/Source/SharedPreview/SharedThumbnailRenderer.h

@@ -33,6 +33,15 @@ namespace AZ
             ~SharedThumbnailRenderer();
 
         private:
+            struct ThumbnailConfig
+            {
+                Data::AssetId m_modelId;
+                Data::AssetId m_materialId;
+                Data::AssetId m_lightingId;
+            };
+
+            ThumbnailConfig GetThumbnailConfig(AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey);
+
             //! ThumbnailerRendererRequestsBus::Handler interface overrides...
             void RenderThumbnail(AzToolsFramework::Thumbnailer::SharedThumbnailKey thumbnailKey, int thumbnailSize) override;
             bool Installed() const override;
@@ -42,15 +51,12 @@ namespace AZ
 
             // Default assets to be kept loaded and used for rendering if not overridden
             static constexpr const char* DefaultLightingPresetPath = "lightingpresets/thumbnail.lightingpreset.azasset";
-            const Data::AssetId DefaultLightingPresetAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(DefaultLightingPresetPath);
             Data::Asset<RPI::AnyAsset> m_defaultLightingPresetAsset;
 
             static constexpr const char* DefaultModelPath = "models/sphere.azmodel";
-            const Data::AssetId DefaultModelAssetId = AZ::RPI::AssetUtils::GetAssetIdForProductPath(DefaultModelPath);
             Data::Asset<RPI::ModelAsset> m_defaultModelAsset;
 
             static constexpr const char* DefaultMaterialPath = "";
-            const Data::AssetId DefaultMaterialAssetId;
             Data::Asset<RPI::MaterialAsset> m_defaultMaterialAsset;
         };
     } // namespace LyIntegration

+ 5 - 2
Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/NodePalette/TreeItems/NodePaletteTreeItem.cpp

@@ -9,6 +9,10 @@
 
 #include <GraphCanvas/Widgets/NodePalette/TreeItems/NodePaletteTreeItem.h>
 
+AZ_PUSH_DISABLE_WARNING(4244 4251 4800, "-Wunknown-warning-option")
+#include <QIcon>
+AZ_POP_DISABLE_WARNING
+
 namespace GraphCanvas
 {
     ////////////////////////
@@ -19,7 +23,6 @@ namespace GraphCanvas
 
     NodePaletteTreeItem::NodePaletteTreeItem(AZStd::string_view name, EditorId editorId)
         : GraphCanvas::GraphCanvasTreeItem()
-        , m_errorIcon(":/GraphCanvasEditorResources/toast_error_icon.png")
         , m_editorId(editorId)
         , m_name(QString::fromUtf8(name.data(), static_cast<int>(name.size())))
         , m_selected(false)
@@ -88,7 +91,7 @@ namespace GraphCanvas
             case Qt::DecorationRole:
                 if (HasError())
                 {
-                    return m_errorIcon;
+                    return QIcon(":/GraphCanvasEditorResources/toast_error_icon.png");
                 }
                 break;
             default:

+ 0 - 5
Gems/GraphCanvas/Code/StaticLib/GraphCanvas/Widgets/NodePalette/TreeItems/NodePaletteTreeItem.h

@@ -9,10 +9,6 @@
 
 #include <AzCore/PlatformIncl.h>
 
-AZ_PUSH_DISABLE_WARNING(4244 4251 4800, "-Wunknown-warning-option")
-#include <QIcon>
-AZ_POP_DISABLE_WARNING
-
 #include <AzCore/RTTI/RTTI.h>
 #include <AzCore/std/string/string.h>
 #include <AzCore/std/string/string_view.h>
@@ -113,7 +109,6 @@ namespace GraphCanvas
     private:
 
         // Error Display
-        QIcon m_errorIcon;
         QString m_errorString;
 
         AZStd::string m_styleOverride;

+ 1 - 0
Gems/ScriptCanvas/Code/Editor/View/Widgets/NodePalette/NodePaletteModel.cpp

@@ -881,6 +881,7 @@ namespace ScriptCanvasEditor
 
     void NodePaletteModel::RepopulateModel()
     {
+        AZ_PROFILE_FUNCTION(ScriptCanvas);
         ClearRegistry();
 
         PopulateNodePaletteModel((*this));

+ 2 - 0
Gems/ScriptCanvas/Code/Editor/View/Windows/MainWindow.cpp

@@ -428,6 +428,8 @@ namespace ScriptCanvasEditor
         , m_closeCurrentGraphAfterSave(false)
         , m_styleManager(ScriptCanvasEditor::AssetEditorId, "ScriptCanvas/StyleSheet/graphcanvas_style.json")
     {
+        AZ_PROFILE_FUNCTION(ScriptCanvas);
+
         VariablePaletteRequestBus::Handler::BusConnect();
         GraphCanvas::AssetEditorAutomationRequestBus::Handler::BusConnect(ScriptCanvasEditor::AssetEditorId);