瀏覽代碼

Action Manager | Migrate the View menu from the legacy system (#11162)

* Implement the View > Viewport menu actions

Signed-off-by: Danilo Aimini <[email protected]>

* Add Refresh Style action to View menu

Signed-off-by: Danilo Aimini <[email protected]>

* Migrate Layout actions

Signed-off-by: Danilo Aimini <[email protected]>

* Add unit tests for new API calls.

Signed-off-by: Danilo Aimini <[email protected]>

* Fix reference passing in erase_if lambdas

Signed-off-by: Danilo Aimini <[email protected]>

* Expose the default bookmark counter and use it to inform the number fo bookmarks

Signed-off-by: Danilo Aimini <[email protected]>
Danilo Aimini 3 年之前
父節點
當前提交
a88f38d3df

+ 513 - 6
Code/Editor/Core/EditorActionsHandler.cpp

@@ -16,6 +16,8 @@
 #include <AzToolsFramework/ActionManager/Menu/MenuManagerInterface.h>
 #include <AzToolsFramework/ActionManager/Menu/MenuManagerInternalInterface.h>
 #include <AzToolsFramework/ActionManager/ToolBar/ToolBarManagerInterface.h>
+#include <AzToolsFramework/Viewport/LocalViewBookmarkLoader.h>
+#include <AzToolsFramework/Viewport/ViewportSettings.h>
 
 #include <AzQtComponents/Components/SearchLineEdit.h>
 #include <AzQtComponents/Components/Style.h>
@@ -23,6 +25,7 @@
 #include <CryEdit.h>
 #include <EditorCoreAPI.h>
 #include <Editor/Undo/Undo.h>
+#include <Editor/EditorViewportCamera.h>
 #include <Editor/EditorViewportSettings.h>
 #include <GameEngine.h>
 #include <LmbrCentral/Audio/AudioSystemComponentBus.h>
@@ -41,9 +44,11 @@
 static constexpr AZStd::string_view EditorMainWindowActionContextIdentifier = "o3de.context.editor.mainwindow";
 
 static constexpr AZStd::string_view AngleSnappingStateChangedUpdaterIdentifier = "o3de.updater.onAngleSnappingStateChanged";
+static constexpr AZStd::string_view DrawHelpersStateChangedUpdaterIdentifier = "o3de.updater.onViewportDrawHelpersStateChanged";
 static constexpr AZStd::string_view EntitySelectionChangedUpdaterIdentifier = "o3de.updater.onEntitySelectionChanged";
 static constexpr AZStd::string_view GameModeStateChangedUpdaterIdentifier = "o3de.updater.onGameModeStateChanged";
 static constexpr AZStd::string_view GridSnappingStateChangedUpdaterIdentifier = "o3de.updater.onGridSnappingStateChanged";
+static constexpr AZStd::string_view IconsStateChangedUpdaterIdentifier = "o3de.updater.onViewportIconsStateChanged";
 static constexpr AZStd::string_view LevelLoadedUpdaterIdentifier = "o3de.updater.onLevelLoaded";
 static constexpr AZStd::string_view RecentFilesChangedUpdaterIdentifier = "o3de.updater.onRecentFilesChanged";
 static constexpr AZStd::string_view UndoRedoUpdaterIdentifier = "o3de.updater.onUndoRedo";
@@ -63,6 +68,10 @@ static constexpr AZStd::string_view GameAudioMenuIdentifier = "o3de.menu.editor.
 static constexpr AZStd::string_view GameDebuggingMenuIdentifier = "o3de.menu.editor.game.debugging";
 static constexpr AZStd::string_view ToolsMenuIdentifier = "o3de.menu.editor.tools";
 static constexpr AZStd::string_view ViewMenuIdentifier = "o3de.menu.editor.view";
+static constexpr AZStd::string_view LayoutsMenuIdentifier = "o3de.menu.editor.view.layouts";
+static constexpr AZStd::string_view ViewportMenuIdentifier = "o3de.menu.editor.viewport";
+static constexpr AZStd::string_view GoToLocationMenuIdentifier = "o3de.menu.editor.goToLocation";
+static constexpr AZStd::string_view SaveLocationMenuIdentifier = "o3de.menu.editor.saveLocation";
 static constexpr AZStd::string_view HelpMenuIdentifier = "o3de.menu.editor.help";
 static constexpr AZStd::string_view HelpDocumentationMenuIdentifier = "o3de.menu.editor.help.documentation";
 static constexpr AZStd::string_view HelpGameDevResourcesMenuIdentifier = "o3de.menu.editor.help.gamedevresources";
@@ -78,7 +87,7 @@ bool IsLevelLoaded()
     return !cryEdit->IsExportingLegacyData() && GetIEditor()->IsLevelLoaded();
 }
 
-bool IsEntitySelected()
+bool AreEntitiesSelected()
 {
     bool result = false;
     AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
@@ -86,7 +95,12 @@ bool IsEntitySelected()
     return result;
 }
 
-void EditorActionsHandler::Initialize(QMainWindow* mainWindow)
+static bool CompareLayoutNames(const QString& name1, const QString& name2)
+{
+    return name1.compare(name2, Qt::CaseInsensitive) < 0;
+}
+
+void EditorActionsHandler::Initialize(MainWindow* mainWindow)
 {
     m_mainWindow = mainWindow;
     m_cryEditApp = CCryEditApp::instance();
@@ -120,6 +134,20 @@ void EditorActionsHandler::Initialize(QMainWindow* mainWindow)
     InitializeMenus();
     InitializeToolBars();
 
+    // Retrieve the bookmark count from the loader.
+    m_defaultBookmarkCount = AzToolsFramework::LocalViewBookmarkLoader::DefaultViewBookmarkCount;
+
+    // Ensure the layouts menu is refreshed when the layouts list changes.
+    QObject::connect(
+        m_mainWindow->m_viewPaneManager, &QtViewPaneManager::savedLayoutsChanged, m_mainWindow,
+        [&]()
+        {
+            RefreshLayoutActions();
+        }
+    );
+
+    RefreshLayoutActions();
+
     // Ensure the tools menu and toolbar are refreshed when the viewpanes change.
     QObject::connect(
         m_qtViewPaneManager, &QtViewPaneManager::registeredPanesChanged, m_mainWindow,
@@ -129,9 +157,11 @@ void EditorActionsHandler::Initialize(QMainWindow* mainWindow)
         }
     );
 
+    const int DefaultViewportId = 0;
     AzToolsFramework::EditorEventsBus::Handler::BusConnect();
     AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusConnect();
     AzToolsFramework::ToolsApplicationNotificationBus::Handler::BusConnect();
+    AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Handler::BusConnect(DefaultViewportId);
     m_initialized = true;
 }
 
@@ -139,6 +169,7 @@ EditorActionsHandler::~EditorActionsHandler()
 {
     if (m_initialized)
     {
+        AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Handler::BusDisconnect();
         AzToolsFramework::ToolsApplicationNotificationBus::Handler::BusDisconnect();
         AzToolsFramework::EditorEntityContextNotificationBus::Handler::BusDisconnect();
         AzToolsFramework::EditorEventsBus::Handler::BusDisconnect();
@@ -156,9 +187,11 @@ void EditorActionsHandler::InitializeActionContext()
 void EditorActionsHandler::InitializeActionUpdaters()
 {
     m_actionManagerInterface->RegisterActionUpdater(AngleSnappingStateChangedUpdaterIdentifier);
+    m_actionManagerInterface->RegisterActionUpdater(DrawHelpersStateChangedUpdaterIdentifier);
     m_actionManagerInterface->RegisterActionUpdater(EntitySelectionChangedUpdaterIdentifier);
     m_actionManagerInterface->RegisterActionUpdater(GameModeStateChangedUpdaterIdentifier);
     m_actionManagerInterface->RegisterActionUpdater(GridSnappingStateChangedUpdaterIdentifier);
+    m_actionManagerInterface->RegisterActionUpdater(IconsStateChangedUpdaterIdentifier);
     m_actionManagerInterface->RegisterActionUpdater(RecentFilesChangedUpdaterIdentifier);
     m_actionManagerInterface->RegisterActionUpdater(UndoRedoUpdaterIdentifier);
 
@@ -708,7 +741,7 @@ void EditorActionsHandler::InitializeActions()
             }
         );
 
-        m_actionManagerInterface->InstallEnabledStateCallback("o3de.action.game.exportSelectedObjects", IsEntitySelected);
+        m_actionManagerInterface->InstallEnabledStateCallback("o3de.action.game.exportSelectedObjects", AreEntitiesSelected);
         m_actionManagerInterface->AddActionToUpdater(EntitySelectionChangedUpdaterIdentifier, "o3de.action.game.exportSelectedObjects");
     }
 
@@ -793,6 +826,202 @@ void EditorActionsHandler::InitializeActions()
         );
     }
 
+    // --- View Actions
+
+    // Component Entity Layout
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.layout.componentEntityLayout";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Component Entity Layout (Default)";
+        actionProperties.m_category = "Layout";
+
+        m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            [this]()
+            {
+                m_mainWindow->m_viewPaneManager->RestoreDefaultLayout();
+            }
+        );
+    }
+
+    // Save Layout...
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.layout.save";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Save Layout...";
+        actionProperties.m_description = "Save the current layout.";
+        actionProperties.m_category = "Layout";
+
+        m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            [this]()
+            {
+                m_mainWindow->SaveLayout();
+            }
+        );
+    }
+
+    // Restore Default Layout
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.layout.restoreDefault";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Restore Default Layout";
+        actionProperties.m_description = "Restored the default layout for the Editor.";
+        actionProperties.m_category = "Layout";
+
+        m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            [this]()
+            {
+                m_mainWindow->m_viewPaneManager->RestoreDefaultLayout(true);
+            }
+        );
+    }
+
+    // Go to Position...
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.view.goToPosition";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Go to Position...";
+        actionProperties.m_description = "Move the editor camera to the position and rotation provided.";
+        actionProperties.m_category = "View";
+        actionProperties.m_hideFromMenusWhenDisabled = false;
+
+        m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            [cryEdit = m_cryEditApp]()
+            {
+                cryEdit->OnDisplayGotoPosition();
+            }
+        );
+
+        m_actionManagerInterface->InstallEnabledStateCallback(actionIdentifier, IsLevelLoaded);
+        m_actionManagerInterface->AddActionToUpdater(LevelLoadedUpdaterIdentifier, actionIdentifier);
+    }
+
+    // Center on Selection
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.view.centerOnSelection";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Center on Selection";
+        actionProperties.m_description = "Center the viewport to show selected entities.";
+        actionProperties.m_category = "View";
+        actionProperties.m_hideFromMenusWhenDisabled = false;
+
+        m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            []()
+            {
+                AzToolsFramework::EditorRequestBus::Broadcast(&AzToolsFramework::EditorRequestBus::Events::GoToSelectedEntitiesInViewports);
+            }
+        );
+
+        m_actionManagerInterface->InstallEnabledStateCallback(actionIdentifier, AreEntitiesSelected);
+        m_actionManagerInterface->AddActionToUpdater(EntitySelectionChangedUpdaterIdentifier, actionIdentifier);
+
+        m_hotKeyManagerInterface->SetActionHotKey(actionIdentifier, "Z");
+    }
+
+    // View Bookmarks
+    InitializeViewBookmarkActions();
+
+    // Show Helpers
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.view.toggleHelpers";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Show Helpers";
+        actionProperties.m_description = "Show/Hide Helpers.";
+        actionProperties.m_category = "View";
+
+        m_actionManagerInterface->RegisterCheckableAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            []()
+            {
+                AzToolsFramework::SetHelpersVisible(!AzToolsFramework::HelpersVisible());
+                AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Broadcast(
+                    &AzToolsFramework::ViewportInteraction::ViewportSettingNotifications::OnDrawHelpersChanged,
+                    AzToolsFramework::HelpersVisible());
+            },
+            []()
+            {
+                return AzToolsFramework::HelpersVisible();
+            }
+        );
+
+        m_actionManagerInterface->AddActionToUpdater(DrawHelpersStateChangedUpdaterIdentifier, actionIdentifier);
+
+        m_hotKeyManagerInterface->SetActionHotKey(actionIdentifier, "Shift+Space");
+    }
+
+    // Show Icons
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.view.toggleIcons";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Show Icons";
+        actionProperties.m_description = "Show/Hide Icons.";
+        actionProperties.m_category = "View";
+
+        m_actionManagerInterface->RegisterCheckableAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            []()
+            {
+                AzToolsFramework::SetIconsVisible(!AzToolsFramework::IconsVisible());
+                AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Broadcast(
+                    &AzToolsFramework::ViewportInteraction::ViewportSettingNotifications::OnIconsVisibilityChanged,
+                    AzToolsFramework::IconsVisible());
+            },
+            []()
+            {
+                return AzToolsFramework::IconsVisible();
+            }
+        );
+
+        m_actionManagerInterface->AddActionToUpdater(IconsStateChangedUpdaterIdentifier, actionIdentifier);
+
+        m_hotKeyManagerInterface->SetActionHotKey(actionIdentifier, "Ctrl+Space");
+    }
+
+    // Refresh Style
+    {
+        const AZStd::string_view& actionIdentifier = "o3de.action.view.refreshEditorStyle";
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = "Refresh Style";
+        actionProperties.m_description = "Refreshes the editor stylesheet.";
+        actionProperties.m_category = "View";
+
+        m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            []()
+            {
+                GetIEditor()->Notify(eNotify_OnStyleChanged);
+            }
+        );
+    }
+
     // --- Help Actions
 
     // Tutorials
@@ -1066,6 +1295,26 @@ void EditorActionsHandler::InitializeMenus()
         menuProperties.m_name = "&View";
         m_menuManagerInterface->RegisterMenu(ViewMenuIdentifier, menuProperties);
     }
+        {
+            AzToolsFramework::MenuProperties menuProperties;
+            menuProperties.m_name = "Layouts";
+            m_menuManagerInterface->RegisterMenu(LayoutsMenuIdentifier, menuProperties);
+        }
+        {
+            AzToolsFramework::MenuProperties menuProperties;
+            menuProperties.m_name = "Viewport";
+            m_menuManagerInterface->RegisterMenu(ViewportMenuIdentifier, menuProperties);
+        }
+            {
+                AzToolsFramework::MenuProperties menuProperties;
+                menuProperties.m_name = "Go to Location";
+                m_menuManagerInterface->RegisterMenu(GoToLocationMenuIdentifier, menuProperties);
+            }
+            {
+                AzToolsFramework::MenuProperties menuProperties;
+                menuProperties.m_name = "Save Location";
+                m_menuManagerInterface->RegisterMenu(SaveLocationMenuIdentifier, menuProperties);
+            }
     {
         AzToolsFramework::MenuProperties menuProperties;
         menuProperties.m_name = "&Help";
@@ -1135,8 +1384,8 @@ void EditorActionsHandler::InitializeMenus()
         {
             m_menuManagerInterface->AddSubMenuToMenu(EditModifyMenuIdentifier, EditModifySnapMenuIdentifier, 100);
             {
-                m_menuManagerInterface->AddActionToMenu(EditModifySnapMenuIdentifier, "o3de.action.edit.snap.toggleAngleSnapping", 100);
-                m_menuManagerInterface->AddActionToMenu(EditModifySnapMenuIdentifier, "o3de.action.edit.snap.toggleGridSnapping", 200);
+                m_menuManagerInterface->AddActionToMenu(EditModifySnapMenuIdentifier, "o3de.action.edit.snap.toggleGridSnapping", 100);
+                m_menuManagerInterface->AddActionToMenu(EditModifySnapMenuIdentifier, "o3de.action.edit.snap.toggleAngleSnapping", 200);
             }
             m_menuManagerInterface->AddSubMenuToMenu(EditModifyMenuIdentifier, EditModifyModesMenuIdentifier, 200);
         }
@@ -1173,6 +1422,48 @@ void EditorActionsHandler::InitializeMenus()
         }
     }
 
+    // View
+
+
+    {
+        m_menuManagerInterface->AddSubMenuToMenu(ViewMenuIdentifier, LayoutsMenuIdentifier, 100);
+        {
+            m_menuManagerInterface->AddActionToMenu(LayoutsMenuIdentifier, "o3de.action.layout.componentEntityLayout", 100);
+            m_menuManagerInterface->AddSeparatorToMenu(LayoutsMenuIdentifier, 200);
+
+            // Some of the contents of the Layouts menu are initialized in RefreshLayoutActions.
+
+            m_menuManagerInterface->AddSeparatorToMenu(LayoutsMenuIdentifier, 400);
+            m_menuManagerInterface->AddActionToMenu(LayoutsMenuIdentifier, "o3de.action.layout.save", 500);
+            m_menuManagerInterface->AddActionToMenu(LayoutsMenuIdentifier, "o3de.action.layout.restoreDefault", 600);
+        }
+        m_menuManagerInterface->AddSubMenuToMenu(ViewMenuIdentifier, ViewportMenuIdentifier, 200);
+        {
+            m_menuManagerInterface->AddActionToMenu(ViewportMenuIdentifier, "o3de.action.view.goToPosition", 100);
+            m_menuManagerInterface->AddActionToMenu(ViewportMenuIdentifier, "o3de.action.view.centerOnSelection", 200);
+            m_menuManagerInterface->AddSubMenuToMenu(ViewportMenuIdentifier, GoToLocationMenuIdentifier, 300);
+            {
+                for (int index = 0; index < m_defaultBookmarkCount; ++index)
+                {
+                    const AZStd::string actionIdentifier = AZStd::string::format("o3de.action.view.bookmark[%i].goTo", index);
+                    m_menuManagerInterface->AddActionToMenu(GoToLocationMenuIdentifier, actionIdentifier, 0);
+                }
+            }
+            m_menuManagerInterface->AddSubMenuToMenu(ViewportMenuIdentifier, SaveLocationMenuIdentifier, 400);
+            {
+                for (int index = 0; index < m_defaultBookmarkCount; ++index)
+                {
+                    const AZStd::string actionIdentifier = AZStd::string::format("o3de.action.view.bookmark[%i].save", index);
+                    m_menuManagerInterface->AddActionToMenu(SaveLocationMenuIdentifier, actionIdentifier, 0);
+                }
+            }
+            m_menuManagerInterface->AddSeparatorToMenu(ViewportMenuIdentifier, 500);
+            m_menuManagerInterface->AddActionToMenu(ViewportMenuIdentifier, "o3de.action.view.toggleHelpers", 600);
+            m_menuManagerInterface->AddActionToMenu(ViewportMenuIdentifier, "o3de.action.view.toggleIcons", 700);
+        }
+        m_menuManagerInterface->AddActionToMenu(ViewMenuIdentifier, "o3de.action.view.refreshEditorStyle", 300);
+    }
+
     // Help
     {
         m_menuManagerInterface->AddWidgetToMenu(HelpMenuIdentifier, "o3de.widgetAction.help.searchDocumentation", 100);
@@ -1356,11 +1647,21 @@ void EditorActionsHandler::OnAngleSnappingChanged([[maybe_unused]] bool enabled)
     m_actionManagerInterface->TriggerActionUpdater(AngleSnappingStateChangedUpdaterIdentifier);
 }
 
+void EditorActionsHandler::OnDrawHelpersChanged([[maybe_unused]] bool enabled)
+{
+    m_actionManagerInterface->TriggerActionUpdater(DrawHelpersStateChangedUpdaterIdentifier);
+}
+
 void EditorActionsHandler::OnGridSnappingChanged([[maybe_unused]] bool enabled)
 {
     m_actionManagerInterface->TriggerActionUpdater(GridSnappingStateChangedUpdaterIdentifier);
 }
 
+void EditorActionsHandler::OnIconsVisibilityChanged([[maybe_unused]] bool enabled)
+{
+    m_actionManagerInterface->TriggerActionUpdater(IconsStateChangedUpdaterIdentifier);
+}
+
 bool EditorActionsHandler::IsRecentFileActionActive(int index)
 {
     RecentFileList* recentFiles = m_cryEditApp->GetRecentFileList();
@@ -1391,6 +1692,115 @@ void EditorActionsHandler::UpdateRecentFileActions()
     m_actionManagerInterface->TriggerActionUpdater(RecentFilesChangedUpdaterIdentifier);
 }
 
+void EditorActionsHandler::RefreshLayoutActions()
+{
+    m_menuManagerInterface->RemoveSubMenusFromMenu(LayoutsMenuIdentifier, m_layoutMenuIdentifiers);
+    m_layoutMenuIdentifiers.clear();
+
+    // Place all sub-menus in the same sort index in the menu.
+    // This will display them in order of addition (alphabetical) and ensure no external tool can add items in-between
+    const int sortKey = 300;
+
+    QStringList layoutNames = m_mainWindow->m_viewPaneManager->LayoutNames();
+    std::sort(layoutNames.begin(), layoutNames.end(), CompareLayoutNames);
+
+    for (const auto& layoutName : layoutNames)
+    {
+        AZStd::string layoutMenuIdentifier = AZStd::string::format("o3de.menu.layout[%s]", layoutName.toUtf8().data());
+
+        // Create the menu and related actions for the layout if they do not already exist.
+        if (!m_menuManagerInterface->IsMenuRegistered(layoutMenuIdentifier))
+        {
+            AzToolsFramework::MenuProperties menuProperties;
+            menuProperties.m_name = layoutName.toUtf8().data();
+            m_menuManagerInterface->RegisterMenu(layoutMenuIdentifier, menuProperties);
+
+            {
+                AZStd::string actionIdentifier = AZStd::string::format("o3de.action.layout[%s].load", layoutName.toUtf8().data());
+                AzToolsFramework::ActionProperties actionProperties;
+                actionProperties.m_name = "Load";
+                actionProperties.m_description = AZStd::string::format("Load the \"%s\" layout.", layoutName.toUtf8().data());
+                actionProperties.m_category = "Layout";
+
+                m_actionManagerInterface->RegisterAction(
+                    EditorMainWindowActionContextIdentifier,
+                    actionIdentifier,
+                    actionProperties,
+                    [layout = layoutName, this]()
+                    {
+                        m_mainWindow->ViewLoadPaneLayout(layout);
+                    }
+                );
+
+                m_menuManagerInterface->AddActionToMenu(layoutMenuIdentifier, actionIdentifier, 0);
+            }
+
+            {
+                AZStd::string actionIdentifier = AZStd::string::format("o3de.action.layout[%s].save", layoutName.toUtf8().data());
+                AzToolsFramework::ActionProperties actionProperties;
+                actionProperties.m_name = "Save";
+                actionProperties.m_description = AZStd::string::format("Save the \"%s\" layout.", layoutName.toUtf8().data());
+                actionProperties.m_category = "Layout";
+
+                m_actionManagerInterface->RegisterAction(
+                    EditorMainWindowActionContextIdentifier,
+                    actionIdentifier,
+                    actionProperties,
+                    [layout = layoutName, this]()
+                    {
+                        m_mainWindow->ViewSavePaneLayout(layout);
+                    }
+                );
+
+                m_menuManagerInterface->AddActionToMenu(layoutMenuIdentifier, actionIdentifier, 100);
+            }
+
+            {
+                AZStd::string actionIdentifier = AZStd::string::format("o3de.action.layout[%s].rename", layoutName.toUtf8().data());
+                AzToolsFramework::ActionProperties actionProperties;
+                actionProperties.m_name = "Rename...";
+                actionProperties.m_description = AZStd::string::format("Rename the \"%s\" layout.", layoutName.toUtf8().data());
+                actionProperties.m_category = "Layout";
+
+                m_actionManagerInterface->RegisterAction(
+                    EditorMainWindowActionContextIdentifier,
+                    actionIdentifier,
+                    actionProperties,
+                    [layout = layoutName, this]()
+                    {
+                        m_mainWindow->ViewRenamePaneLayout(layout);
+                    }
+                );
+
+                m_menuManagerInterface->AddActionToMenu(layoutMenuIdentifier, actionIdentifier, 200);
+            }
+
+            {
+                AZStd::string actionIdentifier = AZStd::string::format("o3de.action.layout[%s].delete", layoutName.toUtf8().data());
+                AzToolsFramework::ActionProperties actionProperties;
+                actionProperties.m_name = "Delete";
+                actionProperties.m_description = AZStd::string::format("Delete the \"%s\" layout.", layoutName.toUtf8().data());
+                actionProperties.m_category = "Layout";
+
+                m_actionManagerInterface->RegisterAction(
+                    EditorMainWindowActionContextIdentifier,
+                    actionIdentifier,
+                    actionProperties,
+                    [layout = layoutName, this]()
+                    {
+                        m_mainWindow->ViewDeletePaneLayout(layout);
+                    }
+                );
+
+                m_menuManagerInterface->AddActionToMenu(layoutMenuIdentifier, actionIdentifier, 300);
+            }
+        }
+
+        m_layoutMenuIdentifiers.push_back(layoutMenuIdentifier);
+        m_menuManagerInterface->AddSubMenuToMenu(LayoutsMenuIdentifier, layoutMenuIdentifier, sortKey);
+    }
+}
+
 void EditorActionsHandler::RefreshToolActions()
 {
     // If the tools are being displayed in the menu or toolbar already, remove them.
@@ -1418,7 +1828,7 @@ void EditorActionsHandler::RefreshToolActions()
         AZStd::string toolActionIdentifier = AZStd::string::format("o3de.action.tool.%s", viewpane.m_name.toUtf8().data());
 
         // Create the action if it does not already exist.
-        if (m_actionManagerInternalInterface->GetAction(toolActionIdentifier) == nullptr)
+        if (!m_actionManagerInterface->IsActionRegistered(toolActionIdentifier))
         {
             AzToolsFramework::ActionProperties actionProperties;
             actionProperties.m_name =
@@ -1459,3 +1869,100 @@ void EditorActionsHandler::RefreshToolActions()
     m_menuManagerInterface->AddActionsToMenu(ToolsMenuIdentifier, toolsMenuItems);
     m_toolBarManagerInterface->AddActionsToToolBar(ToolsToolBarIdentifier, toolsToolBarItems);
 }
+
+void EditorActionsHandler::InitializeViewBookmarkActions()
+{
+    // --- Go to Location
+    for (int index = 0; index < m_defaultBookmarkCount; ++index)
+    {
+        const AZStd::string actionIdentifier = AZStd::string::format("o3de.action.view.bookmark[%i].goTo", index);
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = AZStd::string::format("Go to Location %i", index+1);
+        actionProperties.m_description = AZStd::string::format("Go to Location %i.", index+1);
+        actionProperties.m_category = "View Bookmark";
+        actionProperties.m_hideFromMenusWhenDisabled = false;
+
+        auto outcome = m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            [index]()
+            {
+                AzToolsFramework::ViewBookmarkInterface* viewBookmarkInterface = AZ::Interface<AzToolsFramework::ViewBookmarkInterface>::Get();
+                if (!viewBookmarkInterface)
+                {
+                    AZ_Warning("Main Window", false, "Couldn't find View Bookmark Loader");
+                    return false;
+                }
+
+                const AZStd::optional<AzToolsFramework::ViewBookmark> bookmark = viewBookmarkInterface->LoadBookmarkAtIndex(index);
+                if (!bookmark.has_value())
+                {
+                    return false;
+                }
+
+                // Check the bookmark we want to load is not exactly 0.
+                if (bookmark.value().IsZero())
+                {
+                    QString tagConsoleText = QObject::tr("View Bookmark %1 has not been set yet").arg(index + 1);
+                    AZ_Warning("Main Window", false, tagConsoleText.toUtf8().data());
+                    return false;
+                }
+
+                SandboxEditor::InterpolateDefaultViewportCameraToTransform(
+                    bookmark->m_position, AZ::DegToRad(bookmark->m_rotation.GetX()), AZ::DegToRad(bookmark->m_rotation.GetZ()));
+
+                QString tagConsoleText = QObject::tr("View Bookmark %1 loaded position: x=%2, y=%3, z=%4")
+                                             .arg(index + 1)
+                                             .arg(bookmark->m_position.GetX(), 0, 'f', 2)
+                                             .arg(bookmark->m_position.GetY(), 0, 'f', 2)
+                                             .arg(bookmark->m_position.GetZ(), 0, 'f', 2);
+
+                AZ_Printf("MainWindow", tagConsoleText.toUtf8().data());
+                return true;
+            }
+        );
+
+        m_actionManagerInterface->InstallEnabledStateCallback(actionIdentifier, IsLevelLoaded);
+        m_actionManagerInterface->AddActionToUpdater(LevelLoadedUpdaterIdentifier, actionIdentifier);
+
+        m_hotKeyManagerInterface->SetActionHotKey(actionIdentifier, AZStd::string::format("Shift+F%i", index+1));
+    }
+
+    // --- Save Location
+    for (int index = 0; index < m_defaultBookmarkCount; ++index)
+    {
+        const AZStd::string actionIdentifier = AZStd::string::format("o3de.action.view.bookmark[%i].save", index);
+
+        AzToolsFramework::ActionProperties actionProperties;
+        actionProperties.m_name = AZStd::string::format("Save Location %i", index+1);
+        actionProperties.m_description = AZStd::string::format("Save Location %i.", index+1);
+        actionProperties.m_category = "View Bookmark";
+        actionProperties.m_hideFromMenusWhenDisabled = false;
+
+        m_actionManagerInterface->RegisterAction(
+            EditorMainWindowActionContextIdentifier,
+            actionIdentifier,
+            actionProperties,
+            [index]()
+            {
+                if (auto viewBookmark = AzToolsFramework::StoreViewBookmarkFromActiveCameraAtIndex(index); viewBookmark.has_value())
+                {
+                    const QString tagConsoleText = QObject::tr("View Bookmark %1 set to the position: x=%2, y=%3, z=%4")
+                                                       .arg(index + 1)
+                                                       .arg(viewBookmark->m_position.GetX(), 0, 'f', 2)
+                                                       .arg(viewBookmark->m_position.GetY(), 0, 'f', 2)
+                                                       .arg(viewBookmark->m_position.GetZ(), 0, 'f', 2);
+
+                    AZ_Printf("MainWindow", tagConsoleText.toUtf8().data());
+                }
+            }
+        );
+
+        m_actionManagerInterface->InstallEnabledStateCallback(actionIdentifier, IsLevelLoaded);
+        m_actionManagerInterface->AddActionToUpdater(LevelLoadedUpdaterIdentifier, actionIdentifier);
+
+        m_hotKeyManagerInterface->SetActionHotKey(actionIdentifier, AZStd::string::format("Ctrl+F%i", index+1));
+    }
+}

+ 14 - 3
Code/Editor/Core/EditorActionsHandler.h

@@ -16,6 +16,7 @@
 #include <AzToolsFramework/Viewport/ViewportMessages.h>
 
 class CCryEditApp;
+class MainWindow;
 class QMainWindow;
 class QtViewPaneManager;
 class QWidget;
@@ -37,7 +38,7 @@ class EditorActionsHandler
     , private AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Handler
 {
 public:
-    void Initialize(QMainWindow* mainWindow);
+    void Initialize(MainWindow* mainWindow);
     ~EditorActionsHandler();
 
 private:
@@ -64,12 +65,17 @@ private:
     // ToolsApplicationNotificationBus overrides ...
     void AfterEntitySelectionChanged(
         const AzToolsFramework::EntityIdList& newlySelectedEntities, const AzToolsFramework::EntityIdList& newlyDeselectedEntities) override;
-    virtual void AfterUndoRedo() override;
+    void AfterUndoRedo() override;
     void OnEndUndo(const char* label, bool changed) override;
 
     // ViewportSettingsNotificationBus overrides ...
     void OnAngleSnappingChanged(bool enabled) override;
+    void OnDrawHelpersChanged(bool enabled) override;
     void OnGridSnappingChanged(bool enabled) override;
+    void OnIconsVisibilityChanged(bool enabled) override;
+
+    // Layouts
+    void RefreshLayoutActions();
 
     // Recent Files
     bool IsRecentFileActionActive(int index);
@@ -78,6 +84,10 @@ private:
     // Tools
     void RefreshToolActions();
 
+    // View Bookmarks
+    int m_defaultBookmarkCount = 12;
+    void InitializeViewBookmarkActions();
+
     bool m_initialized = false;
 
     // Editor Action Manager initialization functions
@@ -89,9 +99,10 @@ private:
     AzToolsFramework::ToolBarManagerInterface* m_toolBarManagerInterface = nullptr;
 
     CCryEditApp* m_cryEditApp;
-    QMainWindow* m_mainWindow;
+    MainWindow* m_mainWindow;
     QtViewPaneManager* m_qtViewPaneManager;
 
+    AZStd::vector<AZStd::string> m_layoutMenuIdentifiers;
     AZStd::vector<AZStd::string> m_toolActionIdentifiers;
 
     bool m_isPrefabSystemEnabled = false;

+ 2 - 0
Code/Editor/CryEdit.h

@@ -241,6 +241,8 @@ protected:
     // -------------------------------------------
 
 private:
+    friend class EditorActionsHandler;
+
     void InitLevel(const CEditCommandLineInfo& cmdInfo);
 
     bool ConnectToAssetProcessor() const;

+ 2 - 2
Code/Editor/EditorViewportWidget.cpp

@@ -936,6 +936,7 @@ void EditorViewportWidget::SetViewportId(int id)
                 id, &AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Events::OnGridSnappingChanged, snapping);
         }
     );
+    m_editorViewportSettingsCallbacks->SetGridSnappingChangedEvent(m_gridSnappingHandler);
 
     m_angleSnappingHandler = SandboxEditor::AngleSnappingChangedEvent::Handler(
         [id](const bool snapping)
@@ -944,8 +945,7 @@ void EditorViewportWidget::SetViewportId(int id)
                 id, &AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Events::OnAngleSnappingChanged, snapping);
         }
     );
-
-    m_editorViewportSettingsCallbacks->SetGridSnappingChangedEvent(m_gridSnappingHandler);
+    m_editorViewportSettingsCallbacks->SetAngleSnappingChangedEvent(m_angleSnappingHandler);
 }
 
 void EditorViewportWidget::ConnectViewportInteractionRequestBus()

+ 3 - 0
Code/Editor/MainWindow.cpp

@@ -1065,6 +1065,9 @@ void MainWindow::InitActions()
             []()
             {
                 AzToolsFramework::SetIconsVisible(!AzToolsFramework::IconsVisible());
+                AzToolsFramework::ViewportInteraction::ViewportSettingsNotificationBus::Broadcast(
+                    &AzToolsFramework::ViewportInteraction::ViewportSettingNotifications::OnIconsVisibilityChanged,
+                    AzToolsFramework::IconsVisible());
             });
 
     // Audio actions

+ 2 - 0
Code/Editor/MainWindow.h

@@ -231,6 +231,8 @@ private Q_SLOTS:
     void OnOpenAssetImporterManager(const QStringList& list);
 
 private:
+    friend class EditorActionsHandler;
+
     bool IsGemEnabled(const QString& uuid, const QString& version) const;
 
     // Broadcast the SystemTick event

+ 34 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ActionManager/Menu/EditorMenu.cpp

@@ -51,14 +51,47 @@ namespace AzToolsFramework
         }
 
         int sortKey = sortKeyIterator->second;
+        bool removed = false;
 
         AZStd::erase_if(
             m_menuItems[sortKey],
-            [actionIdentifier](const MenuItem& item)
+            [&](const MenuItem& item)
             {
+                removed = true;
                 return item.m_identifier == actionIdentifier;
             }
         );
+
+        if (removed)
+        {
+            m_actionToSortKeyMap.erase(actionIdentifier);
+        }
+    }
+
+    void EditorMenu::RemoveSubMenu(AZStd::string menuIdentifier)
+    {
+        auto sortKeyIterator = m_subMenuToSortKeyMap.find(menuIdentifier);
+        if (sortKeyIterator == m_subMenuToSortKeyMap.end())
+        {
+            return;
+        }
+
+        int sortKey = sortKeyIterator->second;
+        bool removed = false;
+
+        AZStd::erase_if(
+            m_menuItems[sortKey],
+            [&](const MenuItem& item)
+            {
+                removed = true;
+                return item.m_identifier == menuIdentifier;
+            }
+        );
+
+        if (removed)
+        {
+            m_subMenuToSortKeyMap.erase(menuIdentifier);
+        }
     }
 
     void EditorMenu::AddSubMenu(int sortKey, AZStd::string menuIdentifier)

+ 1 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ActionManager/Menu/EditorMenu.h

@@ -50,6 +50,7 @@ namespace AzToolsFramework
 
         // Remove Menu Items
         void RemoveAction(AZStd::string actionIdentifier);
+        void RemoveSubMenu(AZStd::string menuIdentifier);
 
         // Returns whether the action or menu queried is contained in this menu.
         bool ContainsAction(const AZStd::string& actionIdentifier) const;

+ 122 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ActionManager/Menu/MenuManager.cpp

@@ -85,6 +85,11 @@ namespace AzToolsFramework
         return AZ::Success();
     }
 
+    bool MenuManager::IsMenuRegistered(const AZStd::string& menuIdentifier) const
+    {
+        return m_menus.contains(menuIdentifier);
+    }
+
     MenuManagerOperationResult MenuManager::AddActionToMenu(
         const AZStd::string& menuIdentifier, const AZStd::string& actionIdentifier, int sortIndex)
     {
@@ -280,6 +285,123 @@ namespace AzToolsFramework
         return AZ::Success();
     }
 
+    MenuManagerOperationResult MenuManager::AddSubMenusToMenu(const AZStd::string& menuIdentifier, const AZStd::vector<AZStd::pair<AZStd::string, int>>& subMenus)
+    {
+        auto menuIterator = m_menus.find(menuIdentifier);
+        if (menuIterator == m_menus.end())
+        {
+            return AZ::Failure(AZStd::string::format(
+                "Menu Manager - Could not add sub-menus to menu \"%s\" - menu has not been registered.", menuIdentifier.c_str()));
+        }
+
+        AZStd::string errorMessage = AZStd::string::format(
+            "Menu Manager - Errors on AddSubMenusToMenu for menu \"%s\" - some sub-menus were not added:", menuIdentifier.c_str());
+        bool couldNotAddSubMenu = false;
+
+        for (const auto& pair : subMenus)
+        {
+            if (!IsMenuRegistered(pair.first))
+            {
+                errorMessage += AZStd::string(" ") + pair.first;
+                couldNotAddSubMenu = true;
+                continue;
+            }
+
+            if (menuIterator->second.ContainsSubMenu(pair.first))
+            {
+                errorMessage += AZStd::string(" ") + pair.first;
+                couldNotAddSubMenu = true;
+                continue;
+            }
+
+            menuIterator->second.AddSubMenu(pair.second, pair.first);
+        }
+
+        m_menusToRefresh.insert(menuIdentifier);
+
+        if (couldNotAddSubMenu)
+        {
+            return AZ::Failure(errorMessage);
+        }
+
+        return AZ::Success();
+    }
+
+    MenuManagerOperationResult MenuManager::RemoveSubMenuFromMenu(const AZStd::string& menuIdentifier, const AZStd::string& subMenuIdentifier)
+    {
+        auto menuIterator = m_menus.find(menuIdentifier);
+        if (menuIterator == m_menus.end())
+        {
+            return AZ::Failure(AZStd::string::format(
+                "Menu Manager - Could not remove sub-menu \"%s\" from menu \"%s\" - menu has not been registered.",
+                subMenuIdentifier.c_str(),
+                menuIdentifier.c_str()));
+        }
+
+        if (!IsMenuRegistered(subMenuIdentifier))
+        {
+            return AZ::Failure(AZStd::string::format(
+                "Menu Manager - Could not remove sub-menu \"%s\" from menu \"%s\" - sub-menu has not been registered.",
+                subMenuIdentifier.c_str(),
+                menuIdentifier.c_str()));
+        }
+
+        if (!menuIterator->second.ContainsSubMenu(subMenuIdentifier))
+        {
+            return AZ::Failure(AZStd::string::format(
+                "Menu Manager - Could not remove sub-menu \"%s\" from menu \"%s\" - menu does not contain sub-menu.",
+                subMenuIdentifier.c_str(),
+                menuIdentifier.c_str()));
+        }
+
+        menuIterator->second.RemoveSubMenu(subMenuIdentifier);
+
+        m_menusToRefresh.insert(menuIdentifier);
+        return AZ::Success();
+    }
+
+    MenuManagerOperationResult MenuManager::RemoveSubMenusFromMenu(const AZStd::string& menuIdentifier, const AZStd::vector<AZStd::string>& subMenuIdentifiers)
+    {
+        auto menuIterator = m_menus.find(menuIdentifier);
+        if (menuIterator == m_menus.end())
+        {
+            return AZ::Failure(AZStd::string::format(
+                "Menu Manager - Could not remove sub-menus from menu \"%s\" - menu has not been registered.", menuIdentifier.c_str()));
+        }
+
+        AZStd::string errorMessage = AZStd::string::format(
+            "Menu Manager - Errors on RemoveSubMenusFromMenu for menu \"%s\" - some sub-menus were not removed:", menuIdentifier.c_str());
+        bool couldNotRemoveSubMenus = false;
+
+        for (const AZStd::string& subMenuIdentifier : subMenuIdentifiers)
+        {
+            if (!IsMenuRegistered(subMenuIdentifier))
+            {
+                errorMessage += AZStd::string(" ") + subMenuIdentifier;
+                couldNotRemoveSubMenus = true;
+                continue;
+            }
+
+            if (!menuIterator->second.ContainsSubMenu(subMenuIdentifier))
+            {
+                errorMessage += AZStd::string(" ") + subMenuIdentifier;
+                couldNotRemoveSubMenus = true;
+                continue;
+            }
+
+            menuIterator->second.RemoveSubMenu(subMenuIdentifier);
+        }
+
+        m_menusToRefresh.insert(menuIdentifier);
+
+        if (couldNotRemoveSubMenus)
+        {
+            return AZ::Failure(errorMessage);
+        }
+
+        return AZ::Success();
+    }
+
     MenuManagerOperationResult MenuManager::AddMenuToMenuBar(const AZStd::string& menuBarIdentifier, const AZStd::string& menuIdentifier, int sortIndex)
     {
         auto menuBarIterator = m_menuBars.find(menuBarIdentifier);

+ 7 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ActionManager/Menu/MenuManager.h

@@ -40,6 +40,7 @@ namespace AzToolsFramework
         // MenuManagerInterface overrides ...
         MenuManagerOperationResult RegisterMenu(const AZStd::string& menuIdentifier, const MenuProperties& properties) override;
         MenuManagerOperationResult RegisterMenuBar(const AZStd::string& menuBarIdentifier) override;
+        bool IsMenuRegistered(const AZStd::string& menuIdentifier) const override;
         MenuManagerOperationResult AddActionToMenu(
             const AZStd::string& menuIdentifier, const AZStd::string& actionIdentifier, int sortIndex) override;
         MenuManagerOperationResult AddActionsToMenu(
@@ -51,6 +52,12 @@ namespace AzToolsFramework
         MenuManagerOperationResult AddSeparatorToMenu(const AZStd::string& menuIdentifier, int sortIndex) override;
         MenuManagerOperationResult AddSubMenuToMenu(
             const AZStd::string& menuIdentifier, const AZStd::string& subMenuIdentifier, int sortIndex) override;
+        MenuManagerOperationResult AddSubMenusToMenu(
+            const AZStd::string& menuIdentifier, const AZStd::vector<AZStd::pair<AZStd::string, int>>& subMenus) override;
+        MenuManagerOperationResult RemoveSubMenuFromMenu(
+            const AZStd::string& menuIdentifier, const AZStd::string& subMenuIdentifier) override;
+        MenuManagerOperationResult RemoveSubMenusFromMenu(
+            const AZStd::string& menuIdentifier, const AZStd::vector<AZStd::string>& subMenuIdentifiers) override;
         MenuManagerOperationResult AddWidgetToMenu(
             const AZStd::string& menuIdentifier, const AZStd::string& widgetActionIdentifier, int sortIndex) override;
         MenuManagerOperationResult AddMenuToMenuBar(

+ 26 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/ActionManager/Menu/MenuManagerInterface.h

@@ -46,6 +46,11 @@ namespace AzToolsFramework
         //! @return A successful outcome object, or a string with a message detailing the error in case of failure.
         virtual MenuManagerOperationResult RegisterMenuBar(const AZStd::string& menuBarIdentifier) = 0;
 
+        //! Returns whether a menu with the identifier queried is registered to the Menu Manager.
+        //! @param menuIdentifier The identifier for the menu to query.
+        //! @return True if a Menu with the identifier provided was found, false otherwise.
+        virtual bool IsMenuRegistered(const AZStd::string& menuIdentifier) const = 0;
+        
         //! Add an Action to a Menu. Will prompt an update of the menu.
         //! @param menuIdentifier The identifier for the menu the action is being added to.
         //! @param actionIdentifier The identifier for the action to add to the menu.
@@ -90,6 +95,27 @@ namespace AzToolsFramework
         virtual MenuManagerOperationResult AddSubMenuToMenu(
             const AZStd::string& menuIdentifier, const AZStd::string& subMenuIdentifier, int sortIndex) = 0;
 
+        //! Add multiple Sub-Menus to a Menu.
+        //! @param menuIdentifier The identifier for the menu the sub-menus are being added to.
+        //! @param actions A vector of pairs of identifiers for the sub-menus to add to the menu and their sort position.
+        //! @return A successful outcome object, or a string with a message detailing the error in case of failure.
+        virtual MenuManagerOperationResult AddSubMenusToMenu(
+            const AZStd::string& menuIdentifier, const AZStd::vector<AZStd::pair<AZStd::string, int>>& subMenus) = 0;
+
+        //! Removes a Sub-Menu from a Menu.
+        //! @param menuIdentifier The identifier for the menu the sub-menu is being removed from.
+        //! @param subMenuIdentifier The identifier for the sub-menu to remove from the menu.
+        //! @return A successful outcome object, or a string with a message detailing the error in case of failure.
+        virtual MenuManagerOperationResult RemoveSubMenuFromMenu(
+            const AZStd::string& menuIdentifier, const AZStd::string& subMenuIdentifier) = 0;
+
+        //! Removes multiple Sub-Menus from a Menu.
+        //! @param menuIdentifier The identifier for the menu the sub-menus are being removed from.
+        //! @param subMenuIdentifiers A vector of identifiers for the sub-menus to remove from the menu.
+        //! @return A successful outcome object, or a string with a message detailing the error in case of failure.
+        virtual MenuManagerOperationResult RemoveSubMenusFromMenu(
+            const AZStd::string& menuIdentifier, const AZStd::vector<AZStd::string>& subMenuIdentifiers) = 0;
+
         //! Add a Widget to a Menu.
         //! @param menuIdentifier The identifier for the menu the sub-menu is being added to.
         //! @param widgetActionIdentifier The identifier to the widget to add to the menu.

+ 8 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ActionManager/ToolBar/EditorToolBar.cpp

@@ -60,14 +60,21 @@ namespace AzToolsFramework
         }
 
         int sortKey = sortKeyIterator->second;
+        bool removed = false;
 
         AZStd::erase_if(
             m_toolBarItems[sortKey],
-            [actionIdentifier](const ToolBarItem& item)
+            [&](const ToolBarItem& item)
             {
+                removed = true;
                 return item.m_identifier == actionIdentifier;
             }
         );
+
+        if (removed)
+        {
+            m_actionToSortKeyMap.erase(actionIdentifier);
+        }
     }
 
     void EditorToolBar::AddWidget(int sortKey, AZStd::string widgetActionIdentifier)

+ 0 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/LocalViewBookmarkLoader.cpp

@@ -27,9 +27,6 @@ namespace AzToolsFramework
     static constexpr const char* LocalBookmarksKey = "LocalBookmarks";
     static constexpr const char* LastKnownLocationKey = "LastKnownLocation";
 
-    // temporary value until there is UI to expose the fields
-    static constexpr int DefaultViewBookmarkCount = 12;
-
     struct ViewBookmarkVisitor : AZ::SettingsRegistryInterface::Visitor
     {
         ViewBookmarkVisitor()

+ 3 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/LocalViewBookmarkLoader.h

@@ -45,6 +45,9 @@ namespace AzToolsFramework
         void OverrideStreamWriteFn(StreamWriteFn streamWriteFn) override;
         void OverrideStreamReadFn(StreamReadFn streamReadFn) override;
         void OverrideFileExistsFn(FileExistsFn fileExistsFn) override;
+        
+        // temporary value until there is UI to expose the fields
+        static constexpr int DefaultViewBookmarkCount = 12;
 
     private:
         LocalViewBookmarkComponent* FindOrCreateLocalViewBookmarkComponent();

+ 3 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h

@@ -241,6 +241,9 @@ namespace AzToolsFramework
             virtual void OnDrawHelpersChanged([[maybe_unused]] bool enabled)
             {
             }
+            virtual void OnIconsVisibilityChanged([[maybe_unused]] bool enabled)
+            {
+            }
 
         protected:
             ~ViewportSettingNotifications() = default;

+ 0 - 1
Code/Framework/AzToolsFramework/Tests/ActionManager/ActionManagerTests.cpp

@@ -25,7 +25,6 @@ namespace UnitTest
         EXPECT_TRUE(outcome.IsSuccess());
     }
 
-    
     TEST_F(ActionManagerFixture, VerifyActionContextIsRegistered)
     {
         m_actionManagerInterface->RegisterActionContext("", "o3de.context.test", {}, m_widget);

+ 115 - 0
Code/Framework/AzToolsFramework/Tests/ActionManager/MenuManagerTests.cpp

@@ -27,6 +27,12 @@ namespace UnitTest
         EXPECT_FALSE(outcome.IsSuccess());
     }
 
+    TEST_F(ActionManagerFixture, VerifyMenuIsRegistered)
+    {
+        auto outcome = m_menuManagerInterface->RegisterMenu("o3de.menu.test", {});
+        EXPECT_TRUE(m_menuManagerInterface->IsMenuRegistered("o3de.menu.test"));
+    }
+
     TEST_F(ActionManagerFixture, RegisterMenuBar)
     {
         auto outcome = m_menuManagerInterface->RegisterMenuBar("o3de.menubar.test");
@@ -279,6 +285,115 @@ namespace UnitTest
         auto outcome = m_menuManagerInterface->AddSubMenuToMenu("o3de.menu.testMenu", "o3de.menu.testSubMenu", 42);
         EXPECT_FALSE(outcome.IsSuccess());
     }
+    
+    TEST_F(ActionManagerFixture, AddSubMenusToMenu)
+    {
+        // Register menu and submenus.
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testMenu", {});
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testSubMenu1", {});
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testSubMenu2", {});
+
+        // Add the sub-menus to the menu.
+        AZStd::vector<AZStd::pair<AZStd::string, int>> testMenus;
+        testMenus.emplace_back("o3de.menu.testSubMenu1", 100);
+        testMenus.emplace_back("o3de.menu.testSubMenu2", 200);
+
+        m_menuManagerInterface->AddSubMenusToMenu("o3de.menu.testMenu", testMenus);
+
+        // Manually trigger Menu refresh - Editor will call this once per tick.
+        m_menuManagerInternalInterface->RefreshMenus();
+
+        // Verify the sub-menus are now in the menu.
+        QMenu* menu = m_menuManagerInternalInterface->GetMenu("o3de.menu.testMenu");
+        QMenu* submenu1 = m_menuManagerInternalInterface->GetMenu("o3de.menu.testSubMenu1");
+        QMenu* submenu2 = m_menuManagerInternalInterface->GetMenu("o3de.menu.testSubMenu2");
+        const auto& actions = menu->actions();
+
+        EXPECT_EQ(actions.size(), 2);
+        EXPECT_EQ(actions[0]->menu(), submenu1);
+        EXPECT_EQ(actions[1]->menu(), submenu2);
+    }
+
+    TEST_F(ActionManagerFixture, RemoveSubMenuFromMenu)
+    {
+        // Register menu and submenu.
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testMenu", {});
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testSubMenu", {});
+
+        // Add the sub-menu to the menu.
+        m_menuManagerInterface->AddSubMenuToMenu("o3de.menu.testMenu", "o3de.menu.testSubMenu", 42);
+
+        // Remove the sub-menu from the menu.
+        m_menuManagerInterface->RemoveSubMenuFromMenu("o3de.menu.testMenu", "o3de.menu.testSubMenu");
+
+        // Manually trigger Menu refresh - Editor will call this once per tick.
+        m_menuManagerInternalInterface->RefreshMenus();
+
+        // Verify the sub-menu is not in the menu.
+        QMenu* menu = m_menuManagerInternalInterface->GetMenu("o3de.menu.testMenu");
+        const auto& actions = menu->actions();
+
+        EXPECT_EQ(actions.size(), 0);
+    }
+
+    TEST_F(ActionManagerFixture, RemoveSubMenuFromMenuWithoutAdding)
+    {
+        // Register menu and submenu.
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testMenu", {});
+
+        // Remove the sub-menu from the menu.
+        auto outcome = m_menuManagerInterface->RemoveSubMenuFromMenu("o3de.menu.testMenu", "o3de.menu.testSubMenu");
+        EXPECT_FALSE(outcome.IsSuccess());
+    }
+
+    TEST_F(ActionManagerFixture, RemoveSubMenuFromMenuTwice)
+    {
+        // Register menu and submenu.
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testMenu", {});
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testSubMenu", {});
+
+        // Add the sub-menu to the menu.
+        m_menuManagerInterface->AddSubMenuToMenu("o3de.menu.testMenu", "o3de.menu.testSubMenu", 42);
+
+        // Remove the sub-menu from the menu twice.
+        m_menuManagerInterface->RemoveSubMenuFromMenu("o3de.menu.testMenu", "o3de.menu.testSubMenu");
+        auto outcome = m_menuManagerInterface->RemoveSubMenuFromMenu("o3de.menu.testMenu", "o3de.menu.testSubMenu");
+        EXPECT_FALSE(outcome.IsSuccess());
+    }
+    
+    TEST_F(ActionManagerFixture, RemoveSubMenusFromMenu)
+    {
+        // Register menu and submenus.
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testMenu", {});
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testSubMenu1", {});
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testSubMenu2", {});
+        m_menuManagerInterface->RegisterMenu("o3de.menu.testSubMenu3", {});
+
+        // Add the sub-menus to the menu.
+        AZStd::vector<AZStd::pair<AZStd::string, int>> testMenuAdds;
+        testMenuAdds.emplace_back("o3de.menu.testSubMenu1", 100);
+        testMenuAdds.emplace_back("o3de.menu.testSubMenu2", 200);
+        testMenuAdds.emplace_back("o3de.menu.testSubMenu3", 300);
+
+        m_menuManagerInterface->AddSubMenusToMenu("o3de.menu.testMenu", testMenuAdds);
+
+        // Remove two sub-menus from the menu.
+        AZStd::vector<AZStd::string> testMenuRemoves;
+        testMenuRemoves.emplace_back("o3de.menu.testSubMenu1");
+        testMenuRemoves.emplace_back("o3de.menu.testSubMenu2");
+        m_menuManagerInterface->RemoveSubMenusFromMenu("o3de.menu.testMenu", testMenuRemoves);
+
+        // Manually trigger Menu refresh - Editor will call this once per tick.
+        m_menuManagerInternalInterface->RefreshMenus();
+
+        // Verify only one sub-menu is now in the menu.
+        QMenu* menu = m_menuManagerInternalInterface->GetMenu("o3de.menu.testMenu");
+        QMenu* submenu3 = m_menuManagerInternalInterface->GetMenu("o3de.menu.testSubMenu3");
+        const auto& actions = menu->actions();
+
+        EXPECT_EQ(actions.size(), 1);
+        EXPECT_EQ(actions[0]->menu(), submenu3);
+    }
 
     TEST_F(ActionManagerFixture, AddUnregisteredWidgetInMenu)
     {