You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/LyShine/Code/Editor/EditorWindow.cpp

2404 lines
85 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "EditorCommon.h"
#include "CanvasHelpers.h"
#include "AssetDropHelpers.h"
#include <AzCore/IO/SystemFile.h>
#include <AzCore/std/sort.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/Slice/SliceUtilities.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserEntry.h>
#include <AzQtComponents/Components/StyledDockWidget.h>
#include <AzQtComponents/Components/Widgets/TabWidget.h>
#include <LyShine/UiComponentTypes.h>
#include <LyShine/Bus/UiEditorCanvasBus.h>
#include <Util/PathUtil.h>
#include "EditorDefs.h"
#include "ErrorDialog.h"
#include "Settings.h"
#include "AnchorPresets.h"
#include "PivotPresets.h"
#include "Animation/UiAnimViewDialog.h"
#include "AssetTreeEntry.h"
#include "FindEntityWidget.h"
#include <QMessageBox>
#include <QBoxLayout>
#include <QClipboard>
#include <QUndoGroup>
#include <QScrollBar>
#include <QFileDialog>
#define UICANVASEDITOR_SETTINGS_EDIT_MODE_STATE_KEY (QString("Edit Mode State") + " " + FileHelpers::GetAbsoluteGameDir())
#define UICANVASEDITOR_SETTINGS_EDIT_MODE_GEOM_KEY (QString("Edit Mode Geometry") + " " + FileHelpers::GetAbsoluteGameDir())
#define UICANVASEDITOR_SETTINGS_PREVIEW_MODE_STATE_KEY (QString("Preview Mode State") + " " + FileHelpers::GetAbsoluteGameDir())
#define UICANVASEDITOR_SETTINGS_PREVIEW_MODE_GEOM_KEY (QString("Preview Mode Geometry") + " " + FileHelpers::GetAbsoluteGameDir())
#define UICANVASEDITOR_SETTINGS_WINDOW_STATE_VERSION (1)
#define UICANVASEDITOR_ENTITY_PICKER_CURSOR ":/Icons/EntityPickerCursor.png"
// This has to live outside of any namespaces due to issues on Linux with calls to Q_INIT_RESOURCE if they are inside a namespace
void initUiCanvasEditorResources()
{
Q_INIT_RESOURCE(UiCanvasEditor);
}
namespace
{
//! \brief Writes the current value of the sys_localization_folder CVar to the editor settings file (Amazon.ini)
void SaveStartupLocalizationFolderSetting()
{
if (gEnv && gEnv->pConsole)
{
ICVar* locFolderCvar = gEnv->pConsole->GetCVar("sys_localization_folder");
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
settings.setValue(UICANVASEDITOR_SETTINGS_STARTUP_LOC_FOLDER_KEY, locFolderCvar->GetString());
settings.endGroup();
settings.sync();
}
}
//! \brief Reads loc folder value from Amazon.ini and re-sets the CVar accordingly
void RestoreStartupLocalizationFolderSetting()
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
QString startupLocFolder(settings.value(UICANVASEDITOR_SETTINGS_STARTUP_LOC_FOLDER_KEY).toString());
if (!startupLocFolder.isEmpty() && gEnv && gEnv->pConsole)
{
ICVar* locFolderCvar = gEnv->pConsole->GetCVar("sys_localization_folder");
locFolderCvar->Set(startupLocFolder.toUtf8().constData());
}
settings.endGroup();
settings.sync();
}
}
EditorWindow::UiCanvasEditState::UiCanvasEditState()
: m_inited(false)
{
}
EditorWindow::UiCanvasMetadata::UiCanvasMetadata()
: m_entityContext(nullptr)
, m_undoStack(nullptr)
, m_canvasChangedAndSaved(false)
, m_isSliceEditing(false)
{
}
EditorWindow::UiCanvasMetadata::~UiCanvasMetadata()
{
delete m_entityContext;
delete m_undoStack;
}
EditorWindow::EditorWindow(QWidget* parent, Qt::WindowFlags flags)
: QMainWindow(parent, flags)
, IEditorNotifyListener()
, m_undoGroup(new QUndoGroup(this))
, m_sliceManager(new UiSliceManager(AzFramework::EntityContextId::CreateNull()))
, m_hierarchy(new HierarchyWidget(this))
, m_properties(new PropertiesWrapper(m_hierarchy, this))
, m_canvasTabWidget(nullptr)
, m_canvasTabSectionWidget(nullptr)
, m_viewport(nullptr)
, m_animationWidget(new CUiAnimViewDialog(this))
, m_previewActionLog(new PreviewActionLog(this))
, m_previewAnimationList(new PreviewAnimationList(this))
, m_mainToolbar(new MainToolbar(this))
, m_modeToolbar(new ModeToolbar(this))
, m_enterPreviewToolbar(new EnterPreviewToolbar(this))
, m_previewToolbar(new PreviewToolbar(this))
, m_hierarchyDockWidget(nullptr)
, m_propertiesDockWidget(nullptr)
, m_animationDockWidget(nullptr)
, m_previewActionLogDockWidget(nullptr)
, m_previewAnimationListDockWidget(nullptr)
, m_editorMode(UiEditorMode::Edit)
, m_actionsEnabledWithSelection()
, m_pasteAsSiblingAction(nullptr)
, m_pasteAsChildAction(nullptr)
, m_actionsEnabledWithAlignAllowed()
, m_previewModeCanvasEntityId()
, m_previewModeCanvasSize(0.0f, 0.0f)
, m_clipboardConnection()
, m_newCanvasCount(1)
{
initUiCanvasEditorResources();
// Since the lifetime of EditorWindow and the UI Editor itself aren't the
// same, we use the initial opening of the UI Editor to save the current
// value of the loc folder CVar since the user can temporarily change its
// value while using the UI Editor.
SaveStartupLocalizationFolderSetting();
PropertyHandlers::Register();
setAcceptDrops(true);
// Store local copy of startup localization value
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
m_startupLocFolderName = settings.value(UICANVASEDITOR_SETTINGS_STARTUP_LOC_FOLDER_KEY).toString();
settings.endGroup();
// update menus when the selection changes
connect(m_hierarchy, &HierarchyWidget::SetUserSelection, this, &EditorWindow::UpdateActionsEnabledState);
m_clipboardConnection = connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &EditorWindow::UpdateActionsEnabledState);
// Create the cursor to be used when picking an element in the hierarchy or viewport during object pick mode.
// Uses the default hot spot which is the center of the image
m_entityPickerCursor = QCursor(QPixmap(UICANVASEDITOR_ENTITY_PICKER_CURSOR));
// disable rendering of the editor window until we have restored the window state
setUpdatesEnabled(false);
// Create the viewport widget
m_viewport = new ViewportWidget(this);
m_viewport->GetViewportInteraction()->UpdateZoomFactorLabel();
m_viewport->setFocusPolicy(Qt::StrongFocus);
// Create the central widget
SetupCentralWidget();
// Signal: Hierarchical tree -> Properties pane.
QObject::connect(m_hierarchy,
SIGNAL(SetUserSelection(HierarchyItemRawPtrList*)),
m_properties->GetProperties(),
SLOT(UserSelectionChanged(HierarchyItemRawPtrList*)));
// Signal: Hierarchical tree -> Viewport pane.
QObject::connect(m_hierarchy,
SIGNAL(SetUserSelection(HierarchyItemRawPtrList*)),
GetViewport(),
SLOT(UserSelectionChanged(HierarchyItemRawPtrList*)));
QObject::connect(m_undoGroup, &QUndoGroup::cleanChanged, this, &EditorWindow::CleanChanged);
// by default the BottomDockWidgetArea will be the full width of the main window
// and will make the Hierarchy and Properties panes less tall. This makes the
// Hierarchy and Properties panes occupy the corners and makes the animation pane less wide.
setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea);
setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea);
// Hierarchy pane.
{
m_hierarchyDockWidget = new AzQtComponents::StyledDockWidget("Hierarchy");
m_hierarchyDockWidget->setObjectName("HierarchyDockWidget"); // needed to save state
m_hierarchyDockWidget->setWidget(m_hierarchy);
// needed to get keyboard shortcuts properly
m_hierarchy->setFocusPolicy(Qt::StrongFocus);
addDockWidget(Qt::LeftDockWidgetArea, m_hierarchyDockWidget, Qt::Vertical);
}
// Properties pane.
{
m_propertiesDockWidget = new AzQtComponents::StyledDockWidget("Properties");
m_propertiesDockWidget->setObjectName("PropertiesDockWidget"); // needed to save state
m_propertiesDockWidget->setWidget(m_properties);
m_properties->setFocusPolicy(Qt::StrongFocus);
addDockWidget(Qt::RightDockWidgetArea, m_propertiesDockWidget, Qt::Vertical);
}
// Animation pane.
{
m_animationDockWidget = new AzQtComponents::StyledDockWidget("Animation Editor");
m_animationDockWidget->setObjectName("AnimationDockWidget"); // needed to save state
m_animationDockWidget->setWidget(m_animationWidget);
m_animationWidget->setFocusPolicy(Qt::StrongFocus);
addDockWidget(Qt::BottomDockWidgetArea, m_animationDockWidget, Qt::Horizontal);
}
// Preview action log pane (only shown in preview mode)
{
m_previewActionLogDockWidget = new AzQtComponents::StyledDockWidget("Action Log");
m_previewActionLogDockWidget->setObjectName("PreviewActionLog"); // needed to save state
m_previewActionLogDockWidget->setWidget(m_previewActionLog);
m_previewActionLog->setFocusPolicy(Qt::StrongFocus);
addDockWidget(Qt::BottomDockWidgetArea, m_previewActionLogDockWidget, Qt::Horizontal);
}
// Preview animation list pane (only shown in preview mode)
{
m_previewAnimationListDockWidget = new AzQtComponents::StyledDockWidget("Animation List");
m_previewAnimationListDockWidget->setObjectName("PreviewAnimationList"); // needed to save state
m_previewAnimationListDockWidget->setWidget(m_previewAnimationList);
m_previewAnimationList->setFocusPolicy(Qt::StrongFocus);
addDockWidget(Qt::LeftDockWidgetArea, m_previewAnimationListDockWidget, Qt::Vertical);
}
// We start out in edit mode so hide the preview mode widgets
m_previewActionLogDockWidget->hide();
m_previewAnimationListDockWidget->hide();
m_previewToolbar->hide();
// Initialize the menus
RefreshEditorMenu();
GetIEditor()->RegisterNotifyListener(this);
// Initialize the toolbars
m_viewport->GetViewportInteraction()->InitializeToolbars();
// Start listening for any queries on the UiEditorDLLBus
UiEditorDLLBus::Handler::BusConnect();
// Start listening for any queries on the UiEditorChangeNotificationBus
UiEditorChangeNotificationBus::Handler::BusConnect();
// Start listening for any internal requests and notifications in the UI Editor
UiEditorInternalRequestBus::Handler::BusConnect();
UiEditorInternalNotificationBus::Handler::BusConnect();
AzToolsFramework::AssetBrowser::AssetBrowserModelNotificationBus::Handler::BusConnect();
AzToolsFramework::EditorEvents::Bus::Handler::BusConnect();
FontNotificationBus::Handler::BusConnect();
// Don't draw the viewport until the window is shown
m_viewport->SetRedrawEnabled(false);
// Create an empty canvas
LoadCanvas("", true);
QTimer::singleShot(0, this, SLOT(RestoreEditorWindowSettings()));
}
EditorWindow::~EditorWindow()
{
AzToolsFramework::AssetBrowser::AssetBrowserModelNotificationBus::Handler::BusDisconnect();
AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect();
FontNotificationBus::Handler::BusDisconnect();
QObject::disconnect(m_clipboardConnection);
GetIEditor()->UnregisterNotifyListener(this);
UiEditorDLLBus::Handler::BusDisconnect();
UiEditorChangeNotificationBus::Handler::BusDisconnect();
UiEditorInternalRequestBus::Handler::BusDisconnect();
UiEditorInternalNotificationBus::Handler::BusDisconnect();
// This has to be disconnected, or we'll get some weird feedback loop
// where the cleanChanged signal propagates back up to the EditorWindow's
// tab control, which is possibly already deleted, and everything explodes
QObject::disconnect(m_undoGroup, &QUndoGroup::cleanChanged, this, &EditorWindow::CleanChanged);
// Destroy all loaded canvases
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
DestroyCanvas(*canvasMetadata);
delete canvasMetadata;
}
m_activeCanvasEntityId.SetInvalid();
// Tell the UI animation system that the active canvas has changed
EBUS_EVENT(UiEditorAnimationBus, ActiveCanvasChanged);
// unload the preview mode canvas if it exists (e.g. if we close the editor window while in preview mode)
if (m_previewModeCanvasEntityId.IsValid())
{
gEnv->pLyShine->ReleaseCanvas(m_previewModeCanvasEntityId, false);
}
delete m_sliceLibraryTree;
delete m_sliceManager;
// We must restore the original loc folder CVar value otherwise we will
// have no way of obtaining the original loc folder location (in case
// the user chooses to open the UI Editor once more).
RestoreStartupLocalizationFolderSetting();
}
LyShine::EntityArray EditorWindow::GetSelectedElements()
{
LyShine::EntityArray elements = SelectionHelpers::GetSelectedElements(
m_hierarchy,
m_hierarchy->selectedItems());
return elements;
}
AZ::EntityId EditorWindow::GetActiveCanvasId()
{
return GetCanvas();
}
UndoStack* EditorWindow::GetActiveUndoStack()
{
return GetActiveStack();
}
void EditorWindow::OnEditorTransformPropertiesNeedRefresh()
{
AZ::Uuid transformComponentUuid = LyShine::UiTransform2dComponentUuid;
GetProperties()->TriggerRefresh(AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues, &transformComponentUuid);
}
void EditorWindow::OnEditorPropertiesRefreshEntireTree()
{
GetProperties()->TriggerRefresh(AzToolsFramework::PropertyModificationRefreshLevel::Refresh_EntireTree);
}
void EditorWindow::OpenSourceCanvasFile(QString absolutePathToFile)
{
// If in Preview mode, exit back to Edit mode
if (m_editorMode == UiEditorMode::Preview)
{
ToggleEditorMode();
}
OpenCanvas(absolutePathToFile);
}
AzToolsFramework::EntityIdList EditorWindow::GetSelectedEntityIds()
{
EntityHelpers::EntityIdList entityIds;
if (m_hierarchy)
{
entityIds = SelectionHelpers::GetSelectedElementIds(
m_hierarchy,
m_hierarchy->selectedItems(),
false);
}
return entityIds;
}
AZ::Entity::ComponentArrayType EditorWindow::GetSelectedComponents()
{
AZ::Entity::ComponentArrayType selectedComponents;
if (m_properties)
{
selectedComponents = m_properties->GetProperties()->GetSelectedComponents();
}
return selectedComponents;
}
AZ::EntityId EditorWindow::GetActiveCanvasEntityId()
{
return GetCanvas();
}
void EditorWindow::OnSelectedEntitiesPropertyChanged()
{
// This is necessary to update the PropertiesWidget.
m_hierarchy->SignalUserSelectionHasChanged(m_hierarchy->selectedItems());
}
void EditorWindow::OnBeginUndoableEntitiesChange()
{
AZ_Assert(!m_haveValidCanvasPreChangeState && !m_haveValidEntitiesPreChangeState, "Calling BeginUndoableEntitiesChange before EndUndoableEntitiesChange");
// Check if the canvas is selected to set up the correct undo command
if (m_hierarchy->selectedItems().empty())
{
m_canvasUndoXml = CanvasHelpers::BeginUndoableCanvasChange(m_activeCanvasEntityId);
m_haveValidCanvasPreChangeState = true;
}
else
{
HierarchyClipboard::BeginUndoableEntitiesChange(this, m_preChangeState);
m_haveValidEntitiesPreChangeState = true;
}
}
void EditorWindow::OnEndUndoableEntitiesChange(const AZStd::string& commandText)
{
// Check if the canvas is selected to set up the correct undo command
if (m_hierarchy->selectedItems().empty())
{
AZ_Assert(m_haveValidCanvasPreChangeState, "Calling EndUndoableEntitiesChange without calling BeginUndoableEntitiesChange first");
if (m_haveValidCanvasPreChangeState)
{
CanvasHelpers::EndUndoableCanvasChange(this, commandText.c_str(), m_canvasUndoXml);
m_haveValidCanvasPreChangeState = false;
}
}
else
{
AZ_Assert(m_haveValidEntitiesPreChangeState, "Calling EndUndoableEntitiesChange without calling BeginUndoableEntitiesChange first");
if (m_haveValidEntitiesPreChangeState)
{
HierarchyClipboard::EndUndoableEntitiesChange(this, commandText.c_str(), m_preChangeState);
m_haveValidEntitiesPreChangeState = false;
}
}
}
void EditorWindow::EntryAdded(const AzToolsFramework::AssetBrowser::AssetBrowserEntry* /*entry*/)
{
DeleteSliceLibraryTree();
}
void EditorWindow::EntryRemoved(const AzToolsFramework::AssetBrowser::AssetBrowserEntry* /*entry*/)
{
DeleteSliceLibraryTree();
}
void EditorWindow::OnSliceInstantiated(const AZ::Data::AssetId& sliceAssetId, [[maybe_unused]] const AZ::SliceComponent::SliceInstanceAddress& sliceAddress, [[maybe_unused]] const AzFramework::SliceInstantiationTicket& ticket)
{
// We are only interested in the first tab that is waiting for this slice asset to be instantiated.
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_isSliceEditing && canvasMetadata->m_sliceAssetId == sliceAssetId && !canvasMetadata->m_sliceEntityId.IsValid())
{
// This is the slice instantiation that we do automatically when a slice is opened for edit in a new tab
// Get the entityId of the top level element we have instantiated into the canvas and store it
AZ::EntityId sliceEntityId;
EBUS_EVENT_ID_RESULT(sliceEntityId, canvasMetadata->m_canvasEntityId, UiCanvasBus, GetChildElementEntityId, 0);
canvasMetadata->m_sliceEntityId = sliceEntityId;
// we don't want an asterisk to show as we haven't made any changes to the slice yet
canvasMetadata->m_undoStack->setClean();
// Update the menus for file/save/close - the file menu will show the slice name
RefreshEditorMenu();
// only do this for one slice (in case of the edge case where two slice edit tabs could have been opened before either slice is instantiated)
break;
}
}
// Check if we have any more tabs waiting for their slice to be instantiated for edit (highly unlikely, it would be an edge case)
bool waitingForMoreSliceEditInstantiates = false;
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_isSliceEditing && !canvasMetadata->m_sliceEntityId.IsValid())
{
waitingForMoreSliceEditInstantiates = true;
}
}
if (!waitingForMoreSliceEditInstantiates)
{
UiEditorEntityContextNotificationBus::Handler::BusDisconnect();
}
}
void EditorWindow::OnSliceInstantiationFailed(const AZ::Data::AssetId& sliceAssetId, [[maybe_unused]] const AzFramework::SliceInstantiationTicket& ticket)
{
// We are only interested in the first tab that is waiting for this slice asset to be instantiated.
// It may be impossible to get this error because, in the case of Edit Slice in New Tab, we already have the slice asset loaded
// so it is hard for the instantiate to fail.
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_isSliceEditing && canvasMetadata->m_sliceAssetId == sliceAssetId && !canvasMetadata->m_sliceEntityId.IsValid())
{
// The slice instantiation that failed is an instantiation that we do automatically when a slice is opened for edit in a new tab
// Instantiate failed so close the tab and delete this metadata
UnloadCanvas(canvasMetadata->m_canvasEntityId);
// only do this for one slice (in case of the edge case where two slice edit tabs could have been opened before either slice is instantiated)
break;
}
}
// Check if we have any more tabs waiting for their slice to be instantiated for edit (highly unlikely, it would be an edge case)
bool waitingForMoreSliceEditInstantiates = false;
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_isSliceEditing && !canvasMetadata->m_sliceEntityId.IsValid())
{
waitingForMoreSliceEditInstantiates = true;
}
}
if (!waitingForMoreSliceEditInstantiates)
{
UiEditorEntityContextNotificationBus::Handler::BusDisconnect();
}
}
void EditorWindow::OnEscape()
{
if (GetEditorMode() == UiEditorMode::Preview && isActiveWindow())
{
ToggleEditorMode();
}
}
void EditorWindow::OnFontsReloaded()
{
OnEditorPropertiesRefreshEntireTree();
}
bool EditorWindow::OnPreError(const char* /*window*/, const char* /*fileName*/, int /*line*/, const char* /*func*/, const char* message)
{
AddTraceMessage(message, m_errors);
return true;
}
bool EditorWindow::OnPreWarning(const char* /*window*/, const char* /*fileName*/, int /*line*/, const char* /*func*/, const char* message)
{
AddTraceMessage(message, m_warnings);
return true;
}
void EditorWindow::DestroyCanvas(const UiCanvasMetadata& canvasMetadata)
{
gEnv->pLyShine->ReleaseCanvas(canvasMetadata.m_canvasEntityId, true);
}
bool EditorWindow::IsCanvasTabMetadataValidForTabIndex(int index)
{
QVariant tabData = m_canvasTabWidget->tabBar()->tabData(index);
return tabData.isValid();
}
AZ::EntityId EditorWindow::GetCanvasEntityIdForTabIndex(int index)
{
QVariant tabData = m_canvasTabWidget->tabBar()->tabData(index);
AZ_Assert(tabData.isValid(), "Canvas tab metadata is not valid");
if (tabData.isValid())
{
auto canvasTabMetadata = tabData.value<UiCanvasTabMetadata>();
return canvasTabMetadata.m_canvasEntityId;
}
return AZ::EntityId();
}
int EditorWindow::GetTabIndexForCanvasEntityId(AZ::EntityId canvasEntityId)
{
for (int i = 0; i < m_canvasTabWidget->count(); i++)
{
if (GetCanvasEntityIdForTabIndex(i) == canvasEntityId)
{
return i;
}
}
return -1;
}
EditorWindow::UiCanvasMetadata* EditorWindow::GetCanvasMetadataForTabIndex(int index)
{
return GetCanvasMetadata(GetCanvasEntityIdForTabIndex(index));
}
EditorWindow::UiCanvasMetadata* EditorWindow::GetCanvasMetadata(AZ::EntityId canvasEntityId)
{
auto canvasMetadataMapIt = m_canvasMetadataMap.find(canvasEntityId);
return (canvasMetadataMapIt != m_canvasMetadataMap.end() ? canvasMetadataMapIt->second : nullptr);
}
EditorWindow::UiCanvasMetadata* EditorWindow::GetActiveCanvasMetadata()
{
return GetCanvasMetadata(m_activeCanvasEntityId);
}
AZStd::string EditorWindow::GetCanvasDisplayNameFromAssetPath(const AZStd::string& canvasAssetPathname)
{
QFileInfo fileInfo(canvasAssetPathname.c_str());
QString canvasDisplayName(fileInfo.baseName());
if (canvasDisplayName.isEmpty())
{
canvasDisplayName = QString("Canvas%1").arg(m_newCanvasCount++);
}
return AZStd::string(canvasDisplayName.toLatin1().data());
}
void EditorWindow::HandleCanvasDisplayNameChanged(const UiCanvasMetadata& canvasMetadata)
{
// Update the tab label for the canvas
AZStd::string tabText(canvasMetadata.m_canvasDisplayName);
if (GetChangesHaveBeenMade(canvasMetadata))
{
tabText.append("*");
}
int tabIndex = GetTabIndexForCanvasEntityId(canvasMetadata.m_canvasEntityId);
if (m_canvasTabWidget->tabText(tabIndex) != QString(tabText.c_str()))
{
m_canvasTabWidget->setTabText(tabIndex, tabText.c_str());
}
m_canvasTabWidget->setTabToolTip(tabIndex, canvasMetadata.m_canvasSourceAssetPathname.empty() ? canvasMetadata.m_canvasDisplayName.c_str() : canvasMetadata.m_canvasSourceAssetPathname.c_str());
}
void EditorWindow::CleanChanged([[maybe_unused]] bool clean)
{
UiCanvasMetadata *canvasMetadata = GetActiveCanvasMetadata();
if (canvasMetadata)
{
HandleCanvasDisplayNameChanged(*canvasMetadata);
}
}
bool EditorWindow::SaveCanvasToXml(UiCanvasMetadata& canvasMetadata, bool forceAskingForFilename)
{
AZStd::string sourceAssetPathName = canvasMetadata.m_canvasSourceAssetPathname;
AZStd::string assetIdPathname;
if (canvasMetadata.m_errorsOnLoad)
{
bool saveWithErrors = CanSaveWithErrors(canvasMetadata);
if (!saveWithErrors)
{
return false;
}
}
if (!forceAskingForFilename)
{
// Before saving, make sure the file contains an extension we're expecting
QString filename = sourceAssetPathName.c_str();
if ((!filename.isEmpty()) &&
(!FileHelpers::FilenameHasExtension(filename, UICANVASEDITOR_CANVAS_EXTENSION)))
{
QMessageBox::warning(this, tr("Warning"), tr("Please save with the expected extension: *.%1").arg(UICANVASEDITOR_CANVAS_EXTENSION));
forceAskingForFilename = true;
}
}
if (sourceAssetPathName.empty() || forceAskingForFilename)
{
// Default the pathname to where the current canvas was loaded from or last saved to
QString dir;
QStringList recentFiles = ReadRecentFiles();
// If the canvas we are saving already has a name
if (!sourceAssetPathName.empty())
{
// Default to where it was loaded from or last saved to
// Also notice that we directly assign dir to the filename
// This allows us to have its existing name already entered in
// the File Name field.
dir = sourceAssetPathName.c_str();
}
// Else if we had recently opened canvases, open the most recent one's directory
else if (recentFiles.size() > 0)
{
dir = Path::GetPath(recentFiles.front());
dir.append(canvasMetadata.m_canvasDisplayName.c_str());
}
// Else go to the default canvas directory
else
{
dir = FileHelpers::GetAbsoluteDir(UICANVASEDITOR_CANVAS_DIRECTORY);
dir.append(canvasMetadata.m_canvasDisplayName.c_str());
}
QString filename = QFileDialog::getSaveFileName(nullptr,
QString(),
dir,
"*." UICANVASEDITOR_CANVAS_EXTENSION,
nullptr,
QFileDialog::DontConfirmOverwrite);
if (filename.isEmpty())
{
return false;
}
// Append extension if not present
FileHelpers::AppendExtensionIfNotPresent(filename, UICANVASEDITOR_CANVAS_EXTENSION);
sourceAssetPathName = filename.toUtf8().data();
// Check if the canvas is being saved in the product path
bool foundRelativePath = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(foundRelativePath, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetRelativeProductPathFromFullSourceOrProductPath, sourceAssetPathName, assetIdPathname);
if (!foundRelativePath)
{
// Warn that canvas is being saved outside the product path
int result = QMessageBox::warning(this,
tr("Warning"),
tr("UI canvas %1 is being saved outside the source folder for the project (or the Asset Processor is not running).\n\nSaving to this location will result in not being able to re-open the UI Canvas in the UI Editor from this location.\n\nWould you still like to save to this location?").arg(filename),
(QMessageBox::Save | QMessageBox::Cancel),
QMessageBox::Cancel);
if (result == QMessageBox::Save)
{
assetIdPathname = Path::FullPathToGamePath(sourceAssetPathName.c_str()); // Relative path.
}
else
{
return false;
}
}
}
else
{
sourceAssetPathName = canvasMetadata.m_canvasSourceAssetPathname;
EBUS_EVENT_ID_RESULT(assetIdPathname, canvasMetadata.m_canvasEntityId, UiCanvasBus, GetPathname);
}
FileHelpers::SourceControlAddOrEdit(sourceAssetPathName.c_str(), this);
bool saveSuccessful = false;
EBUS_EVENT_ID_RESULT(saveSuccessful, canvasMetadata.m_canvasEntityId, UiCanvasBus, SaveToXml,
assetIdPathname.c_str(), sourceAssetPathName.c_str());
if (saveSuccessful)
{
AddRecentFile(sourceAssetPathName.c_str());
canvasMetadata.m_errorsOnLoad = false;
if (!canvasMetadata.m_canvasChangedAndSaved)
{
canvasMetadata.m_canvasChangedAndSaved = GetChangesHaveBeenMade(canvasMetadata);
}
canvasMetadata.m_canvasSourceAssetPathname = sourceAssetPathName;
AZStd::string newDisplayName = GetCanvasDisplayNameFromAssetPath(canvasMetadata.m_canvasSourceAssetPathname);
if (canvasMetadata.m_canvasDisplayName != newDisplayName)
{
canvasMetadata.m_canvasDisplayName = newDisplayName;
}
canvasMetadata.m_undoStack->setClean();
// Although the line above will call this if the clean state changed we could be doing a "Save As" of a canvas
// that has no unsaved changes, so the clean state would not change but we want to change the display name.
HandleCanvasDisplayNameChanged(canvasMetadata);
return true;
}
QMessageBox(QMessageBox::Critical,
"Error",
tr("Unable to save %1. Is the file read-only?").arg(sourceAssetPathName.empty() ? "file" : sourceAssetPathName.c_str()),
QMessageBox::Ok, this).exec();
return false;
}
bool EditorWindow::SaveSlice(UiCanvasMetadata& canvasMetadata)
{
// as a safeguard check that the entity still exists
AZ::EntityId sliceEntityId = canvasMetadata.m_sliceEntityId;
AZ::Entity* sliceEntity = nullptr;
EBUS_EVENT_RESULT(sliceEntity, AZ::ComponentApplicationBus, FindEntity, sliceEntityId);
if (!sliceEntity)
{
QMessageBox::critical(this, QObject::tr("Slice Push Failed"), "Slice entity not found in canvas.");
return false;
}
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, sliceEntityId,
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
// if false then something is wrong. The user could have done a detach slice for example
if (!sliceAddress.IsValid() || !sliceAddress.GetReference()->GetSliceAsset())
{
QMessageBox::critical(this, QObject::tr("Slice Push Failed"), "Slice entity no longer appears to be a slice instance.");
return false;
}
// make a list that contains the top-level instanced entity plus all of its descendants
AzToolsFramework::EntityIdList allEntitiesInLocalInstance;
allEntitiesInLocalInstance.push_back(sliceEntityId);
EBUS_EVENT_ID(sliceEntityId, UiElementBus, CallOnDescendantElements,
[&allEntitiesInLocalInstance](const AZ::EntityId id)
{
allEntitiesInLocalInstance.push_back(id);
}
);
const AZ::Outcome<void, AZStd::string> outcome = GetSliceManager()->QuickPushSliceInstance(sliceAddress, allEntitiesInLocalInstance);
if (!outcome)
{
QMessageBox::critical(
this,
QObject::tr("Slice Push Failed"),
outcome.GetError().c_str());
return false;
}
return true;
}
bool EditorWindow::CanSaveWithErrors(const UiCanvasMetadata& canvasMetadata)
{
// Prompt the user that saving may result in data loss. Most of the time this is not desired
// (which is why 'cancel' is the default interaction), but this does provide users a way to still
// save their canvas if this is the only way they can solve the erroneous data
QMessageBox msgBox(this);
msgBox.setText(tr("Canvas %1 loaded with errors. You may lose work if you save.").arg(canvasMetadata.m_canvasDisplayName.c_str()));
msgBox.setInformativeText(tr("Do you want to save your changes?"));
msgBox.setIcon(QMessageBox::Warning);
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
int result = msgBox.exec();
return result == QMessageBox::Save;
}
bool EditorWindow::LoadCanvas(const QString& canvasFilename, bool autoLoad, bool changeActiveCanvasToThis)
{
// Don't allow a new canvas to load if there is a context menu up since loading doesn't
// delete the context menu. Another option is to close the context menu on canvas load,
// but the main editor's behavior seems to be to ignore the main keyboard shortcuts if
// a context menu is up
QWidget* widget = QApplication::activePopupWidget();
if (widget)
{
return false;
}
AZStd::string assetIdPathname;
AZStd::string sourceAssetPathName;
if (!canvasFilename.isEmpty())
{
// Get the relative product path of the canvas to load
bool foundRelativePath = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(foundRelativePath, &AzToolsFramework::AssetSystem::AssetSystemRequest::GetRelativeProductPathFromFullSourceOrProductPath, canvasFilename.toUtf8().data(), assetIdPathname);
if (!foundRelativePath)
{
// Canvas to load is not in a project source folder. Report an error
QMessageBox::critical(this, tr("Error"), tr("Failed to open %1. Please ensure the file resides in a valid source folder for the project and that the Asset Processor is running.").arg(canvasFilename));
return false;
}
// Get the path to the source UI Canvas from the relative product path
// This is done because a canvas could be loaded from the cache folder. In this case, we want to find the path to the source file
bool fullPathfound = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(fullPathfound, &AzToolsFramework::AssetSystemRequestBus::Events::GetFullSourcePathFromRelativeProductPath, assetIdPathname, sourceAssetPathName);
if (!fullPathfound)
{
// Couldn't find the source file. Report an error
QMessageBox::critical(this, tr("Error"), tr("Failed to find the source file for UI canvas %1. Please ensure that the Asset Processor is running and that the source file exists").arg(canvasFilename));
return false;
}
}
// Check if canvas is already loaded
AZ::EntityId alreadyLoadedCanvas;
if (!canvasFilename.isEmpty())
{
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_canvasSourceAssetPathname == sourceAssetPathName)
{
alreadyLoadedCanvas = canvasMetadata->m_canvasEntityId;
break;
}
}
}
if (alreadyLoadedCanvas.IsValid())
{
// Canvas is already loaded
if (changeActiveCanvasToThis)
{
if (CanChangeActiveCanvas())
{
SetActiveCanvas(alreadyLoadedCanvas);
}
}
return true;
}
AZ::EntityId canvasEntityId;
UiEditorEntityContext* entityContext = new UiEditorEntityContext(this);
// Load the canvas
bool errorsOnLoad = false;
if (canvasFilename.isEmpty())
{
canvasEntityId = gEnv->pLyShine->CreateCanvasInEditor(entityContext);
}
else
{
// Collect errors and warnings during the canvas load
AZ::Debug::TraceMessageBus::Handler::BusConnect();
canvasEntityId = gEnv->pLyShine->LoadCanvasInEditor(assetIdPathname.c_str(), sourceAssetPathName.c_str(), entityContext);
// Stop receiving error and warning events
AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
if (canvasEntityId.IsValid())
{
AddRecentFile(sourceAssetPathName.c_str());
CheckForOrphanedChildren(canvasEntityId);
// Show any errors and warnings that occurred during the canvas load
ShowTraceMessages(GetCanvasDisplayNameFromAssetPath(sourceAssetPathName));
errorsOnLoad = !m_errors.empty();
}
else
{
// There was an error loading the file. Report an error
QMessageBox::critical(this, tr("Error"), tr("Failed to load UI canvas %1. See log for details").arg(sourceAssetPathName.c_str()));
}
// Clear any trace messages from the canvas load
ClearTraceMessages();
}
if (!canvasEntityId.IsValid())
{
delete entityContext;
return false;
}
// Add a canvas tab
AZStd::string canvasDisplayName = GetCanvasDisplayNameFromAssetPath(sourceAssetPathName);
int newTabIndex = m_canvasTabWidget->addTab(new QWidget(m_canvasTabWidget), canvasDisplayName.c_str()); // this will call OnCurrentCanvasTabChanged if first tab, but nothing will happen because the metadata won't be set yet
UiCanvasTabMetadata tabMetadata;
tabMetadata.m_canvasEntityId = canvasEntityId;
m_canvasTabWidget->tabBar()->setTabData(newTabIndex, QVariant::fromValue(tabMetadata));
m_canvasTabWidget->setTabToolTip(newTabIndex, sourceAssetPathName.empty() ? canvasDisplayName.c_str() : sourceAssetPathName.c_str());
UiCanvasMetadata* canvasMetadata = new UiCanvasMetadata;
canvasMetadata->m_canvasEntityId = canvasEntityId;
canvasMetadata->m_canvasSourceAssetPathname = sourceAssetPathName;
canvasMetadata->m_canvasDisplayName = canvasDisplayName;
canvasMetadata->m_entityContext = entityContext;
canvasMetadata->m_undoStack = new UndoStack(m_undoGroup);
canvasMetadata->m_autoLoaded = autoLoad;
canvasMetadata->m_errorsOnLoad = errorsOnLoad;
canvasMetadata->m_canvasChangedAndSaved = false;
// Check if there is an automatically created canvas that should be unloaded.
// Unload an automatically created canvas if:
// 1. it's the only loaded canvas
// 2. changes have not been made to it
// 3. the newly loaded canvas is not a new canvas
AZ::EntityId unloadCanvasEntityId;
if (!canvasMetadata->m_canvasSourceAssetPathname.empty())
{
if (m_canvasMetadataMap.size() == 1)
{
UiCanvasMetadata *unloadCanvasMetadata = GetActiveCanvasMetadata();
if (unloadCanvasMetadata && unloadCanvasMetadata->m_autoLoaded)
{
// Check if there are changes to this canvas
if (unloadCanvasMetadata->m_canvasSourceAssetPathname.empty() && !GetChangesHaveBeenMade(*unloadCanvasMetadata))
{
unloadCanvasEntityId = unloadCanvasMetadata->m_canvasEntityId;
}
}
}
}
// Add the newly loaded canvas to the map
m_canvasMetadataMap[canvasEntityId] = canvasMetadata;
// Make the newly loaded canvas the active canvas
if (changeActiveCanvasToThis || !m_activeCanvasEntityId.IsValid())
{
if (CanChangeActiveCanvas())
{
SetActiveCanvas(canvasEntityId);
}
}
// If there was an automatically created empty canvas, unload it
if (unloadCanvasEntityId.IsValid())
{
UnloadCanvas(unloadCanvasEntityId);
}
return true;
}
void EditorWindow::UnloadCanvas(AZ::EntityId canvasEntityId)
{
UiCanvasMetadata* canvasMetadata = GetCanvasMetadata(canvasEntityId);
if (canvasMetadata)
{
// Stop object pick mode so that the hierarchy and viewport states are
// set back to normal before saving the canvas edit state
AzToolsFramework::EditorPickModeRequestBus::Broadcast(
&AzToolsFramework::EditorPickModeRequests::StopEntityPickMode);
// Delete the canvas
DestroyCanvas(*canvasMetadata);
// Remove the undo stack from the undo group
m_undoGroup->removeStack(canvasMetadata->m_undoStack);
// Remove the canvas metadata from the list of loaded canvases
m_canvasMetadataMap.erase(canvasMetadata->m_canvasEntityId);
delete canvasMetadata;
// Remove the tab associated with this canvas
// OnCurrentCanvasTabChanged will be called, and the active canvas will be updated
int tabIndex = GetTabIndexForCanvasEntityId(canvasEntityId);
m_canvasTabWidget->removeTab(tabIndex);
// Ensure the active canvas is valid in case removeTab didn't cause it to change or the implementation changed
if (!GetCanvasMetadata(m_activeCanvasEntityId))
{
if (IsCanvasTabMetadataValidForTabIndex(m_canvasTabWidget->currentIndex()))
{
SetActiveCanvas(GetCanvasEntityIdForTabIndex(m_canvasTabWidget->currentIndex()));
}
else
{
SetActiveCanvas(AZ::EntityId());
}
}
}
}
void EditorWindow::NewCanvas()
{
LoadCanvas("", false);
}
void EditorWindow::OpenCanvas(const QString& canvasFilename)
{
LoadCanvas(canvasFilename, false);
}
void EditorWindow::OpenCanvases(const QStringList& canvasFilenames)
{
for (int i = 0; i < canvasFilenames.size(); i++)
{
LoadCanvas(canvasFilenames.at(i), false, (i == 0));
}
}
void EditorWindow::CloseCanvas(AZ::EntityId canvasEntityId)
{
UiCanvasMetadata* canvasMetadata = GetCanvasMetadata(canvasEntityId);
if (canvasMetadata)
{
if (CanUnloadCanvas(*canvasMetadata))
{
UnloadCanvas(canvasMetadata->m_canvasEntityId);
}
}
}
void EditorWindow::CloseAllCanvases()
{
if (!m_activeCanvasEntityId.IsValid())
{
return;
}
// Check if all canvases can be unloaded
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (!CanUnloadCanvas(*canvasMetadata))
{
return;
}
}
// Make a list of canvases to unload. Unload the active canvas last so that the
// active canvas doesn't keep changing when the canvases are unloaded one by one
AZStd::vector<AZ::EntityId> canvasEntityIds;
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_canvasEntityId != m_activeCanvasEntityId)
{
canvasEntityIds.push_back(canvasMetadata->m_canvasEntityId);
}
}
canvasEntityIds.push_back(m_activeCanvasEntityId);
UnloadCanvases(canvasEntityIds);
}
void EditorWindow::CloseAllOtherCanvases(AZ::EntityId canvasEntityId)
{
if (m_canvasMetadataMap.size() < 2)
{
return;
}
// Check if all but the specified canvas can be unloaded
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_canvasEntityId != canvasEntityId)
{
if (!CanUnloadCanvas(*canvasMetadata))
{
return;
}
}
}
// Make a list of canvases to unload
AZStd::vector<AZ::EntityId> canvasEntityIds;
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_canvasEntityId != canvasEntityId)
{
canvasEntityIds.push_back(canvasMetadata->m_canvasEntityId);
}
}
UnloadCanvases(canvasEntityIds);
// Update the menus for file/save/close
RefreshEditorMenu();
}
bool EditorWindow::CanChangeActiveCanvas()
{
UiCanvasMetadata *canvasMetadata = GetActiveCanvasMetadata();
if (canvasMetadata)
{
if (canvasMetadata->m_entityContext->HasPendingRequests() || canvasMetadata->m_entityContext->IsInstantiatingSlices())
{
return false;
}
}
return true;
}
void EditorWindow::SetActiveCanvas(AZ::EntityId canvasEntityId)
{
// This function is called explicitly to set the current active canvas (when a new canvas is loaded).
// This function is also called from the OnCurrentCanvasTabChanged event handler that is triggered by a user action
// that changes the tab index (closing a tab or clicking on a different tab)
if (canvasEntityId == m_activeCanvasEntityId)
{
return;
}
// Don't redraw the viewport until the active tab has visually changed
m_viewport->SetRedrawEnabled(false);
// Disable previous active canvas
if (m_activeCanvasEntityId.IsValid())
{
UiCanvasMetadata* canvasMetadata = GetActiveCanvasMetadata();
// If the active canvas hasn't been unloaded, stop object pick mode so that the hierarchy
// and viewport states are set back to normal before saving the canvas edit state
AzToolsFramework::EditorPickModeRequestBus::Broadcast(
&AzToolsFramework::EditorPickModeRequests::StopEntityPickMode);
// Disable undo stack
if (canvasMetadata)
{
canvasMetadata->m_undoStack->setActive(false);
}
// Save canvas edit state
SaveActiveCanvasEditState();
}
// Update the active canvas Id
m_activeCanvasEntityId = canvasEntityId;
// Set the current tab index to that of the active canvas.
// If this function was called explicitly (when a new canvas is loaded), setCurrentIndex will trigger OnCurrentCanvasTabChanged, and OnCurrentCanvasTabChanged
// will call us back, but we will early out because the new active canvas will be the same as the current active canvas.
// If this function was called from the OnCurrentCanvasTabChanged event handler (triggered by a user clicking on a tab or a user closing a tab),
// the new tab index will be the same as the current tab index so no more events will be triggered by calling setCurrentIndex here
m_canvasTabWidget->setCurrentIndex(GetTabIndexForCanvasEntityId(m_activeCanvasEntityId));
// Get the new active canvas's metadata
UiCanvasMetadata* canvasMetadata = m_activeCanvasEntityId.IsValid() ? GetCanvasMetadata(m_activeCanvasEntityId) : nullptr;
// Enable new active canvas
if (canvasMetadata)
{
canvasMetadata->m_undoStack->setActive(true);
}
// Update the slice manager
m_sliceManager->SetEntityContextId(canvasMetadata ? canvasMetadata->m_entityContext->GetContextId() : AzFramework::EntityContextId::CreateNull());
// Tell the UI animation system that the active canvas has changed
EBUS_EVENT(UiEditorAnimationBus, ActiveCanvasChanged);
// Clear the hierarchy pane
m_hierarchy->ClearItems();
if (m_activeCanvasEntityId.IsValid())
{
// create the hierarchy tree from the loaded canvas
LyShine::EntityArray childElements;
EBUS_EVENT_ID_RESULT(childElements, m_activeCanvasEntityId, UiCanvasBus, GetChildElements);
m_hierarchy->CreateItems(childElements);
}
m_hierarchy->clearSelection();
m_hierarchy->SetUserSelection(nullptr); // trigger a selection change so the properties updates
m_hierarchy->ActiveCanvasChanged();
m_viewport->ActiveCanvasChanged();
RefreshEditorMenu();
// Restore Canvas edit state
RestoreActiveCanvasEditState();
m_properties->ActiveCanvasChanged();
// Do the rest of the restore after all other events have had a chance to process because
// The hierarchy and properties scrollbars have not been set up yet
QTimer::singleShot(0, this, &EditorWindow::RestoreActiveCanvasEditStatePostEvents);
}
void EditorWindow::SaveActiveCanvasEditState()
{
UiCanvasMetadata* canvasMetadata = GetActiveCanvasMetadata();
if (canvasMetadata)
{
UiCanvasEditState& canvasEditState = canvasMetadata->m_canvasEditState;
// Save viewport state
canvasEditState.m_canvasViewportMatrixProps = m_viewport->GetViewportInteraction()->GetCanvasViewportMatrixProps();
canvasEditState.m_shouldScaleToFitOnViewportResize = m_viewport->GetViewportInteraction()->ShouldScaleToFitOnViewportResize();
canvasEditState.m_viewportInteractionMode = m_viewport->GetViewportInteraction()->GetMode();
canvasEditState.m_viewportCoordinateSystem = m_viewport->GetViewportInteraction()->GetCoordinateSystem();
// Save hierarchy state
const QTreeWidgetItemRawPtrQList& selection = m_hierarchy->selectedItems();
canvasEditState.m_selectedElements = SelectionHelpers::GetSelectedElementIds(m_hierarchy, selection, false);
canvasEditState.m_hierarchyScrollValue = m_hierarchy->verticalScrollBar() ? m_hierarchy->verticalScrollBar()->value() : 0;
// Save properties state
canvasEditState.m_propertiesScrollValue = m_properties->GetProperties()->GetScrollValue();
// Save animation state
canvasEditState.m_uiAnimationEditState.m_time = 0.0f;
canvasEditState.m_uiAnimationEditState.m_timelineScale = 1.0f;
canvasEditState.m_uiAnimationEditState.m_timelineScrollOffset = 0;
EBUS_EVENT_RESULT(canvasEditState.m_uiAnimationEditState, UiEditorAnimationStateBus, GetCurrentEditState);
canvasEditState.m_inited = true;
}
}
void EditorWindow::RestoreActiveCanvasEditState()
{
UiCanvasMetadata* canvasMetadata = GetActiveCanvasMetadata();
if (canvasMetadata)
{
const UiCanvasEditState& canvasEditState = canvasMetadata->m_canvasEditState;
if (canvasEditState.m_inited)
{
// Restore viewport state
m_viewport->GetViewportInteraction()->SetCanvasViewportMatrixProps(canvasEditState.m_canvasViewportMatrixProps);
if (canvasEditState.m_shouldScaleToFitOnViewportResize)
{
m_viewport->GetViewportInteraction()->CenterCanvasInViewport();
}
m_viewport->GetViewportInteraction()->SetCoordinateSystem(canvasEditState.m_viewportCoordinateSystem);
m_viewport->GetViewportInteraction()->SetMode(canvasEditState.m_viewportInteractionMode);
// Restore hierarchy state
HierarchyHelpers::SetSelectedItems(m_hierarchy, &canvasEditState.m_selectedElements);
// Restore animation state
EBUS_EVENT(UiEditorAnimationStateBus, RestoreCurrentEditState, canvasEditState.m_uiAnimationEditState);
}
}
}
void EditorWindow::RestoreActiveCanvasEditStatePostEvents()
{
UiCanvasMetadata* canvasMetadata = GetActiveCanvasMetadata();
if (canvasMetadata)
{
const UiCanvasEditState& canvasEditState = canvasMetadata->m_canvasEditState;
if (canvasEditState.m_inited)
{
// Restore hierarchy state
if (m_hierarchy->verticalScrollBar())
{
m_hierarchy->verticalScrollBar()->setValue(canvasEditState.m_hierarchyScrollValue);
}
// Restore properties state
m_properties->GetProperties()->SetScrollValue(canvasEditState.m_propertiesScrollValue);
}
}
m_viewport->SetRedrawEnabled(true);
m_viewport->setFocus();
}
void EditorWindow::UnloadCanvases(const AZStd::vector<AZ::EntityId>& canvasEntityIds)
{
for (int i = 0; i < canvasEntityIds.size(); i++)
{
UnloadCanvas(canvasEntityIds[i]);
}
}
AZ::EntityId EditorWindow::GetCanvas()
{
return m_activeCanvasEntityId;
}
HierarchyWidget* EditorWindow::GetHierarchy()
{
AZ_Assert(m_hierarchy, "Missing hierarchy widget");
return m_hierarchy;
}
ViewportWidget* EditorWindow::GetViewport()
{
AZ_Assert(m_viewport, "Missing viewport widget");
return m_viewport;
}
PropertiesWidget* EditorWindow::GetProperties()
{
AZ_Assert(m_properties, "Missing properties wrapper");
AZ_Assert(m_properties->GetProperties(), "Missing properties widget");
return m_properties->GetProperties();
}
MainToolbar* EditorWindow::GetMainToolbar()
{
AZ_Assert(m_mainToolbar, "Missing main toolbar");
return m_mainToolbar;
}
ModeToolbar* EditorWindow::GetModeToolbar()
{
AZ_Assert(m_modeToolbar, "Missing mode toolbar");
return m_modeToolbar;
}
EnterPreviewToolbar* EditorWindow::GetEnterPreviewToolbar()
{
AZ_Assert(m_enterPreviewToolbar, "Missing enter preview toolbar");
return m_enterPreviewToolbar;
}
PreviewToolbar* EditorWindow::GetPreviewToolbar()
{
AZ_Assert(m_previewToolbar, "Missing preview toolbar");
return m_previewToolbar;
}
NewElementToolbarSection* EditorWindow::GetNewElementToolbarSection()
{
AZ_Assert(m_mainToolbar, "Missing main toolbar");
return m_mainToolbar->GetNewElementToolbarSection();
}
CoordinateSystemToolbarSection* EditorWindow::GetCoordinateSystemToolbarSection()
{
AZ_Assert(m_mainToolbar, "Missing main toolbar");
return m_mainToolbar->GetCoordinateSystemToolbarSection();
}
CanvasSizeToolbarSection* EditorWindow::GetCanvasSizeToolbarSection()
{
AZ_Assert(m_mainToolbar, "Missing main toolbar");
return m_mainToolbar->GetCanvasSizeToolbarSection();
}
const QCursor& EditorWindow::GetEntityPickerCursor()
{
return m_entityPickerCursor;
}
void EditorWindow::OnEditorNotifyEvent(EEditorNotifyEvent ev)
{
switch (ev)
{
case eNotify_OnIdleUpdate:
m_viewport->Refresh();
break;
case eNotify_OnStyleChanged:
{
// change skin
RefreshEditorMenu();
break;
}
case eNotify_OnUpdateViewports:
{
// provides a way for the animation editor to force updates of the properties dialog during
// an animation
GetProperties()->TriggerRefresh(AzToolsFramework::PropertyModificationRefreshLevel::Refresh_Values);
break;
}
}
}
bool EditorWindow::CanExitNow()
{
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (!CanUnloadCanvas(*canvasMetadata))
{
return false;
}
}
return true;
}
bool EditorWindow::CanUnloadCanvas(UiCanvasMetadata& canvasMetadata)
{
if (GetChangesHaveBeenMade(canvasMetadata))
{
QString name;
if (canvasMetadata.m_isSliceEditing)
{
// This already has "Slice:" prepended to the slice name
name = canvasMetadata.m_canvasDisplayName.c_str();
}
else
{
name = tr("UI canvas \"%1\"").arg(canvasMetadata.m_canvasDisplayName.c_str());
}
const auto defaultButton = QMessageBox::Save;
int result = QMessageBox::question(this,
tr("Save UI Canvas Changes?"),
tr("Would you like to save changes to %1 before closing?").arg(name),
(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel),
defaultButton);
if (result == QMessageBox::Save)
{
bool ok = false;
if (canvasMetadata.m_isSliceEditing)
{
ok = SaveSlice(canvasMetadata);
}
else
{
ok = SaveCanvasToXml(canvasMetadata, false);
}
if (!ok)
{
return false;
}
}
else if (result == QMessageBox::Discard)
{
// Nothing to do
}
else // if( result == QMessageBox::Cancel )
{
return false;
}
}
return true;
}
bool EditorWindow::GetChangesHaveBeenMade(const UiCanvasMetadata& canvasMetadata)
{
return !canvasMetadata.m_undoStack->isClean();
}
QUndoGroup* EditorWindow::GetUndoGroup()
{
return m_undoGroup;
}
UndoStack* EditorWindow::GetActiveStack()
{
return qobject_cast<UndoStack*>(m_undoGroup->activeStack());
}
AssetTreeEntry* EditorWindow::GetSliceLibraryTree()
{
if (!m_sliceLibraryTree)
{
const AZStd::string pathToSearch("ui/slices/library/");
const AZ::Data::AssetType sliceAssetType(AZ::AzTypeInfo<AZ::SliceAsset>::Uuid());
m_sliceLibraryTree = AssetTreeEntry::BuildAssetTree(sliceAssetType, pathToSearch);
}
return m_sliceLibraryTree;
}
AZ::EntityId EditorWindow::GetCanvasForCurrentEditorMode()
{
AZ::EntityId canvasEntityId;
if (GetEditorMode() == UiEditorMode::Edit)
{
canvasEntityId = GetCanvas();
}
else
{
canvasEntityId = GetPreviewModeCanvas();
}
return canvasEntityId;
}
void EditorWindow::ToggleEditorMode()
{
m_editorMode = (m_editorMode == UiEditorMode::Edit) ? UiEditorMode::Preview : UiEditorMode::Edit;
emit EditorModeChanged(m_editorMode);
m_viewport->ClearUntilSafeToRedraw();
if (m_editorMode == UiEditorMode::Edit)
{
// unload the preview mode canvas
if (m_previewModeCanvasEntityId.IsValid())
{
m_previewActionLog->Deactivate();
m_previewAnimationList->Deactivate();
AZ::Entity* entity = nullptr;
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, m_previewModeCanvasEntityId);
if (entity)
{
gEnv->pLyShine->ReleaseCanvas(m_previewModeCanvasEntityId, false);
}
m_previewModeCanvasEntityId.SetInvalid();
}
m_canvasTabSectionWidget->show();
SaveModeSettings(UiEditorMode::Preview, false);
RestoreModeSettings(UiEditorMode::Edit);
}
else
{
// Stop object pick mode
AzToolsFramework::EditorPickModeRequestBus::Broadcast(
&AzToolsFramework::EditorPickModeRequests::StopEntityPickMode);
m_canvasTabSectionWidget->hide();
SaveModeSettings(UiEditorMode::Edit, false);
RestoreModeSettings(UiEditorMode::Preview);
GetPreviewToolbar()->UpdatePreviewCanvasScale(m_viewport->GetPreviewCanvasScale());
// clone the editor canvas to create a temporary preview mode canvas
if (m_activeCanvasEntityId.IsValid())
{
AZ_Assert(!m_previewModeCanvasEntityId.IsValid(), "There is an existing preview mode canvas");
// Get the canvas size
AZ::Vector2 canvasSize = GetPreviewCanvasSize();
if (canvasSize.GetX() == 0.0f && canvasSize.GetY() == 0.0f)
{
// special value of (0,0) means use the viewport size
canvasSize = AZ::Vector2(aznumeric_cast<float>(m_viewport->size().width()), aznumeric_cast<float>(m_viewport->size().height()));
}
AZ::Entity* clonedCanvas = nullptr;
EBUS_EVENT_ID_RESULT(clonedCanvas, m_activeCanvasEntityId, UiCanvasBus, CloneCanvas, canvasSize);
if (clonedCanvas)
{
m_previewModeCanvasEntityId = clonedCanvas->GetId();
}
else
{
QMessageBox::critical(this, "Preview Mode Error", GetEntityContext()->GetErrorMessage().c_str());
// A zero-msec timeout will cause the single-shot timer to execute
// once all events currently in the queue have processed. This allows
// the current "preview mode toggle" to finish and then immediately
// toggle back to edit mode.
const int queueForImmediateExecution = 0;
QTimer::singleShot(queueForImmediateExecution, this,
[this]()
{
ToggleEditorMode();
});
}
}
m_previewActionLog->Activate(m_previewModeCanvasEntityId);
m_previewAnimationList->Activate(m_previewModeCanvasEntityId);
// In Preview mode we want keyboard input to go to to the ViewportWidget so set the
// it to be focused
m_viewport->setFocus();
}
// Update the menus for this mode
RefreshEditorMenu();
}
AZ::Vector2 EditorWindow::GetPreviewCanvasSize()
{
return m_previewModeCanvasSize;
}
void EditorWindow::SetPreviewCanvasSize(AZ::Vector2 previewCanvasSize)
{
m_previewModeCanvasSize = previewCanvasSize;
}
bool EditorWindow::IsPreviewModeToolbar(const QToolBar* toolBar)
{
bool result = false;
if (toolBar == m_previewToolbar)
{
result = true;
}
return result;
}
bool EditorWindow::IsPreviewModeDockWidget(const QDockWidget* dockWidget)
{
bool result = false;
if (dockWidget == m_previewActionLogDockWidget ||
dockWidget == m_previewAnimationListDockWidget)
{
result = true;
}
return result;
}
void EditorWindow::RestoreEditorWindowSettings()
{
// Allow the editor window to draw now that we are ready to restore state.
// Do this before restoring state, otherwise an undocked widget will not be affected by the call
setUpdatesEnabled(true);
RestoreModeSettings(m_editorMode);
m_viewport->SetRedrawEnabled(true);
}
void EditorWindow::SaveEditorWindowSettings()
{
// This saves the dock position, size and visibility of all the dock widgets and tool bars
// for the current mode (it also syncs the settings for the other mode that have already been saved to settings)
SaveModeSettings(m_editorMode, true);
}
UiSliceManager* EditorWindow::GetSliceManager()
{
return m_sliceManager;
}
UiEditorEntityContext* EditorWindow::GetEntityContext()
{
if (GetCanvas().IsValid())
{
auto canvasMetadata = GetActiveCanvasMetadata();
AZ_Assert(canvasMetadata, "Canvas metadata not found");
return canvasMetadata ? canvasMetadata->m_entityContext : nullptr;
}
return nullptr;
}
void EditorWindow::ReplaceEntityContext(UiEditorEntityContext* entityContext)
{
UiCanvasMetadata* canvasMetadata = GetActiveCanvasMetadata();
if (canvasMetadata)
{
delete canvasMetadata->m_entityContext;
canvasMetadata->m_entityContext = entityContext;
m_sliceManager->SetEntityContextId(entityContext->GetContextId());
m_hierarchy->EntityContextChanged();
m_viewport->EntityContextChanged();
}
}
QMenu* EditorWindow::createPopupMenu()
{
QMenu* menu = new QMenu(this);
// Add all QDockWidget panes for the current editor mode
{
QList<QDockWidget*> list = findChildren<QDockWidget*>();
for (auto p : list)
{
// findChildren is recursive, but we only want dock widgets that are immediate children
if (p->parent() == this)
{
bool isPreviewModeDockWidget = IsPreviewModeDockWidget(p);
if (m_editorMode == UiEditorMode::Edit && !isPreviewModeDockWidget ||
m_editorMode == UiEditorMode::Preview && isPreviewModeDockWidget)
{
menu->addAction(p->toggleViewAction());
}
}
}
}
// Add all QToolBar panes for the current editor mode
{
QList<QToolBar*> list = findChildren<QToolBar*>();
for (auto p : list)
{
if (p->parent() == this)
{
bool isPreviewModeToolbar = IsPreviewModeToolbar(p);
if (m_editorMode == UiEditorMode::Edit && !isPreviewModeToolbar ||
m_editorMode == UiEditorMode::Preview && isPreviewModeToolbar)
{
menu->addAction(p->toggleViewAction());
}
}
}
}
return menu;
}
AZ::EntityId EditorWindow::GetCanvasForEntityContext(const AzFramework::EntityContextId& contextId)
{
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
if (canvasMetadata->m_entityContext->GetContextId() == contextId)
{
return canvasMetadata->m_canvasEntityId;
}
}
return AZ::EntityId();
}
void EditorWindow::EditSliceInNewTab(AZ::Data::AssetId sliceAssetId)
{
if (!LoadCanvas("", false))
{
return;
}
AZStd::string assetIdPathname;
EBUS_EVENT_RESULT(assetIdPathname, AZ::Data::AssetCatalogRequestBus, GetAssetPathById, sliceAssetId);
AZStd::string sourceAssetPathName;
bool fullPathfound = false;
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(fullPathfound,
&AzToolsFramework::AssetSystemRequestBus::Events::GetFullSourcePathFromRelativeProductPath, assetIdPathname, sourceAssetPathName);
if (!fullPathfound)
{
sourceAssetPathName = assetIdPathname;
}
AZStd::string canvasDisplayName = "Slice:";
canvasDisplayName += GetCanvasDisplayNameFromAssetPath(sourceAssetPathName);
UiCanvasMetadata* canvasMetadata = GetActiveCanvasMetadata();
canvasMetadata->m_sliceAssetId = sliceAssetId;
canvasMetadata->m_canvasSourceAssetPathname = sourceAssetPathName;
canvasMetadata->m_canvasDisplayName = canvasDisplayName;
canvasMetadata->m_isSliceEditing = true;
HandleCanvasDisplayNameChanged(*canvasMetadata);
// instantiate the slice in the new canvas
AZ::Vector2 viewportPosition(-1.0f,-1.0f); // indicates no viewport position specified
AZ::Data::Asset<AZ::SliceAsset> sliceAsset;
sliceAsset.Create(sliceAssetId, true);
const AzFramework::EntityContextId& entityContextId = canvasMetadata->m_entityContext->GetContextId();
AzFramework::SliceInstantiationTicket ticket;
EBUS_EVENT_ID_RESULT(ticket, entityContextId, UiEditorEntityContextRequestBus, InstantiateEditorSlice, sliceAsset, viewportPosition);
if (ticket.IsValid())
{
// Normally we are only ever waiting for one slice to instantiate for Edit Slice, but there could be an edge case where
// the Instantiate notification is delayed and the user does Edit Slice again.
if (!UiEditorEntityContextNotificationBus::Handler::BusIsConnected())
{
UiEditorEntityContextNotificationBus::Handler::BusConnect();
}
}
}
void EditorWindow::UpdateChangedStatusOnAssetChange(const AzFramework::EntityContextId& contextId, const AZ::Data::Asset<AZ::Data::AssetData>& asset)
{
AZ::EntityId canvasToUpdate = GetCanvasForEntityContext(contextId);
UiCanvasMetadata* canvasMetadata = GetCanvasMetadata(canvasToUpdate);
if (canvasMetadata->m_isSliceEditing && asset.GetType() == AZ::AzTypeInfo<AZ::SliceAsset>::Uuid())
{
// we are in slice edit mode and a slice asset has changed. This could be because we just did a save (push to slice) and the asset
// has been reloaded. Or it could have been pushed to in a different tab.
// Time to do a check to see if there are any remaining overrides on the slice being edited
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, canvasMetadata->m_sliceEntityId,
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
// if false then something is wrong. The user could have done a detach slice for example
if (!sliceAddress.IsValid())
{
return;
}
// as a safeguard check that the entity still exists
AZ::EntityId sliceEntityId = canvasMetadata->m_sliceEntityId;
AZ::Entity* sliceEntity = nullptr;
EBUS_EVENT_RESULT(sliceEntity, AZ::ComponentApplicationBus, FindEntity, sliceEntityId);
if (!sliceEntity)
{
return;
}
// make a list that contains the top-level instanced entity plus all of its descendants
// If entities have been removed they will not be in this list but we will spot the change because the
// m_children member of the parent will have changed.
AzToolsFramework::EntityIdList allEntitiesInLocalInstance;
allEntitiesInLocalInstance.push_back(sliceEntityId);
EBUS_EVENT_ID(sliceEntityId, UiElementBus, CallOnDescendantElements,
[&allEntitiesInLocalInstance](const AZ::EntityId id)
{
allEntitiesInLocalInstance.push_back(id);
}
);
// test if there are any overrides for the slice instance
bool hasOverrides = AzToolsFramework::SliceUtilities::DoEntitiesHaveOverrides( allEntitiesInLocalInstance );
if (!hasOverrides)
{
// if there are no overrides then call setClean on the stack
canvasMetadata->m_undoStack->setClean();
}
}
}
void EditorWindow::EntitiesAddedOrRemoved()
{
// entities have been added or removed to/from the active canvas
UiCanvasMetadata* canvasMetadata = GetActiveCanvasMetadata();
if (canvasMetadata->m_isSliceEditing)
{
// If we are slice editing then it is possible that the change has removed or recreated the slice entity.
// The file menu changes depending on whether the slice entity is valid so update it.
RefreshEditorMenu();
}
}
void EditorWindow::FontTextureHasChanged()
{
// A font texture has changed since we last rendered. Force a render graph update for each loaded canvas.
// Only text components that actually use the affected font will actually regenerate their quads.
for (auto mapItem : m_canvasMetadataMap)
{
auto canvasMetadata = mapItem.second;
EBUS_EVENT_ID(canvasMetadata->m_canvasEntityId, UiCanvasComponentImplementationBus, MarkRenderGraphDirty);
}
if (GetEditorMode() == UiEditorMode::Preview)
{
EBUS_EVENT_ID(GetPreviewModeCanvas(), UiCanvasComponentImplementationBus, MarkRenderGraphDirty);
}
}
void EditorWindow::OnCanvasTabCloseButtonPressed(int index)
{
UiCanvasMetadata* canvasMetadata = GetCanvasMetadataForTabIndex(index);
if (canvasMetadata)
{
if (CanUnloadCanvas(*canvasMetadata))
{
bool isActiveCanvas = (canvasMetadata->m_canvasEntityId == m_activeCanvasEntityId);
UnloadCanvas(canvasMetadata->m_canvasEntityId);
if (!isActiveCanvas)
{
// Update the menus for file/save/close
RefreshEditorMenu();
}
}
}
}
void EditorWindow::OnCurrentCanvasTabChanged(int index)
{
// This is called when the first tab is added, when a tab is removed, or when a user clicks on a tab that's not the current tab
// Get the canvas associated with this index
AZ::EntityId canvasEntityId = IsCanvasTabMetadataValidForTabIndex(index) ? GetCanvasEntityIdForTabIndex(index) : AZ::EntityId();
if (index >= 0 && !canvasEntityId.IsValid())
{
// This occurs when the first tab is added. Since the tab metadata is set after the tab is added, we don't handle this here.
// Instead, SetActiveCanvas is called explicitly when a tab is added
return;
}
if (canvasEntityId.IsValid() && canvasEntityId == m_activeCanvasEntityId)
{
// Nothing else to do. This occurs when a tab is clicked, but the active canvas cannot be changed so the current tab is reverted
// back to the tab of the active canvas
return;
}
if (!CanChangeActiveCanvas())
{
// Set the tab back to that of the active canvas
int activeCanvasIndex = GetTabIndexForCanvasEntityId(m_activeCanvasEntityId);
m_canvasTabWidget->setCurrentIndex(activeCanvasIndex);
QMessageBox::information(this,
tr("Running Slice Operations"),
tr("The current UI canvas is still running slice operations. Please wait until complete before changing tabs."));
return;
}
SetActiveCanvas(canvasEntityId);
}
void EditorWindow::OnCanvasTabContextMenuRequested(const QPoint &point)
{
int tabIndex = m_canvasTabWidget->tabBar()->tabAt(point);
if (tabIndex >= 0)
{
AZ::EntityId canvasEntityId = GetCanvasEntityIdForTabIndex(tabIndex);
UiCanvasMetadata *canvasMetadata = GetCanvasMetadata(canvasEntityId);
QMenu menu(this);
if (canvasMetadata && canvasMetadata->m_isSliceEditing)
{
menu.addAction(CreateSaveSliceAction(canvasMetadata, true));
}
else
{
menu.addAction(CreateSaveCanvasAction(canvasEntityId, true));
menu.addAction(CreateSaveCanvasAsAction(canvasEntityId, true));
}
menu.addAction(CreateSaveAllCanvasesAction(true));
menu.addSeparator();
menu.addAction(CreateCloseCanvasAction(canvasEntityId, true));
menu.addAction(CreateCloseAllCanvasesAction(true));
menu.addAction(CreateCloseAllOtherCanvasesAction(canvasEntityId, true));
menu.addSeparator();
QAction* action = new QAction("Copy Full Path", this);
action->setEnabled(canvasMetadata && !canvasMetadata->m_canvasSourceAssetPathname.empty());
QObject::connect(action,
&QAction::triggered,
this,
[this, canvasEntityId]([[maybe_unused]] bool checked)
{
UiCanvasMetadata *canvasMetadata = GetCanvasMetadata(canvasEntityId);
AZ_Assert(canvasMetadata, "Canvas metadata not found");
if (canvasMetadata)
{
QApplication::clipboard()->setText(canvasMetadata->m_canvasSourceAssetPathname.c_str());
}
});
menu.addAction(action);
menu.exec(m_canvasTabWidget->mapToGlobal(point));
}
else
{
if (m_canvasMetadataMap.size() > 0)
{
QMenu menu(this);
menu.addAction(CreateSaveAllCanvasesAction(true));
menu.addSeparator();
menu.addAction(CreateCloseAllCanvasesAction(true));
menu.exec(m_canvasTabWidget->mapToGlobal(point));
}
}
}
void EditorWindow::SaveModeSettings(UiEditorMode mode, bool syncSettings)
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
if (mode == UiEditorMode::Edit)
{
// save the edit mode state
settings.setValue(UICANVASEDITOR_SETTINGS_EDIT_MODE_STATE_KEY, saveState(UICANVASEDITOR_SETTINGS_WINDOW_STATE_VERSION));
settings.setValue(UICANVASEDITOR_SETTINGS_EDIT_MODE_GEOM_KEY, saveGeometry());
}
else
{
// save the preview mode state
settings.setValue(UICANVASEDITOR_SETTINGS_PREVIEW_MODE_STATE_KEY, saveState(UICANVASEDITOR_SETTINGS_WINDOW_STATE_VERSION));
settings.setValue(UICANVASEDITOR_SETTINGS_PREVIEW_MODE_GEOM_KEY, saveGeometry());
}
settings.endGroup(); // UI canvas editor
if (syncSettings)
{
settings.sync();
}
}
void EditorWindow::RestoreModeSettings(UiEditorMode mode)
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
if (mode == UiEditorMode::Edit)
{
// restore the edit mode state
restoreState(settings.value(UICANVASEDITOR_SETTINGS_EDIT_MODE_STATE_KEY).toByteArray(), UICANVASEDITOR_SETTINGS_WINDOW_STATE_VERSION);
restoreGeometry(settings.value(UICANVASEDITOR_SETTINGS_EDIT_MODE_GEOM_KEY).toByteArray());
}
else
{
// restore the preview mode state
bool stateRestored = restoreState(settings.value(UICANVASEDITOR_SETTINGS_PREVIEW_MODE_STATE_KEY).toByteArray(), UICANVASEDITOR_SETTINGS_WINDOW_STATE_VERSION);
bool geomRestored = restoreGeometry(settings.value(UICANVASEDITOR_SETTINGS_PREVIEW_MODE_GEOM_KEY).toByteArray());
// if either of the above failed then manually hide and show widgets,
// this will happen the first time someone uses preview mode
if (!stateRestored || !geomRestored)
{
m_hierarchyDockWidget->hide();
m_propertiesDockWidget->hide();
m_animationDockWidget->hide();
m_mainToolbar->hide();
m_modeToolbar->hide();
m_enterPreviewToolbar->hide();
m_previewToolbar->show();
m_previewActionLogDockWidget->show();
m_previewAnimationListDockWidget->show();
}
}
settings.endGroup(); // UI canvas editor
}
static const char* UIEDITOR_UNLOAD_SAVED_CANVAS_METRIC_EVENT_NAME = "UiEditorUnloadSavedCanvas";
static const char* UIEDITOR_CANVAS_ID_ATTRIBUTE_NAME = "CanvasId";
static const char* UIEDITOR_CANVAS_WIDTH_METRIC_NAME = "CanvasWidth";
static const char* UIEDITOR_CANVAS_HEIGHT_METRIC_NAME = "CanvasHeight";
static const char* UIEDITOR_CANVAS_MAX_HIERARCHY_DEPTH_METRIC_NAME = "MaxHierarchyDepth";
static const char* UIEDITOR_CANVAS_NUM_ELEMENT_METRIC_NAME = "NumElement";
static const char* UIEDITOR_CANVAS_NUM_ELEMENTS_WITH_COMPONENT_PREFIX_METRIC_NAME = "Num";
static const char* UIEDITOR_CANVAS_NUM_ELEMENTS_WITH_CUSTOM_COMPONENT_METRIC_NAME = "NumCustomElement";
static const char* UIEDITOR_CANVAS_NUM_UNIQUE_CUSTOM_COMPONENT_NAME = "NumUniqueCustomComponent";
static const char* UIEDITOR_CANVAS_NUM_AVAILABLE_CUSTOM_COMPONENT_NAME = "NumAvailableCustomComponent";
static const char* UIEDITOR_CANVAS_NUM_ANCHOR_PRESETS_ATTRIBUTE_NAME = "NumAnchorPreset";
static const char* UIEDITOR_CANVAS_NUM_ANCHOR_CUSTOM_ATTRIBUTE_NAME = "NumAnchorCustom";
static const char* UIEDITOR_CANVAS_NUM_PIVOT_PRESETS_ATTRIBUTE_NAME = "NumPivotPreset";
static const char* UIEDITOR_CANVAS_NUM_PIVOT_CUSTOM_ATTRIBUTE_NAME = "NumPivotCustom";
static const char* UIEDITOR_CANVAS_NUM_ROTATED_ELEMENT_METRIC_NAME = "NumRotatedElement";
static const char* UIEDITOR_CANVAS_NUM_SCALED_ELEMENT_METRIC_NAME = "NumScaledElement";
static const char* UIEDITOR_CANVAS_NUM_SCALE_TO_DEVICE_ELEMENT_METRIC_NAME = "NumScaleToDeviceElement";
int EditorWindow::GetCanvasMaxHierarchyDepth(const LyShine::EntityArray& rootChildElements)
{
int depth = 0;
if (rootChildElements.empty())
{
return depth;
}
int numChildrenCurLevel = rootChildElements.size();
int numChildrenNextLevel = 0;
std::list<AZ::Entity*> elementList(rootChildElements.begin(), rootChildElements.end());
while (!elementList.empty())
{
auto& entity = elementList.front();
LyShine::EntityArray childElements;
EBUS_EVENT_ID_RESULT(childElements, entity->GetId(), UiElementBus, GetChildElements);
if (!childElements.empty())
{
elementList.insert(elementList.end(), childElements.begin(), childElements.end());
numChildrenNextLevel += childElements.size();
}
elementList.pop_front();
numChildrenCurLevel--;
if (numChildrenCurLevel == 0)
{
depth++;
numChildrenCurLevel = numChildrenNextLevel;
numChildrenNextLevel = 0;
}
}
return depth;
}
void EditorWindow::DeleteSliceLibraryTree()
{
// this just deletes the tree so that we know it is dirty
if (m_sliceLibraryTree)
{
delete m_sliceLibraryTree;
m_sliceLibraryTree = nullptr;
}
}
void EditorWindow::paintEvent(QPaintEvent* paintEvent)
{
QMainWindow::paintEvent(paintEvent);
if (m_viewport)
{
m_viewport->Refresh();
}
}
void EditorWindow::closeEvent(QCloseEvent* closeEvent)
{
if (!CanExitNow())
{
// Nothing to do.
closeEvent->ignore();
return;
}
// Save the current window state
SaveEditorWindowSettings();
QMainWindow::closeEvent(closeEvent);
}
void EditorWindow::dragEnterEvent(QDragEnterEvent* event)
{
AssetDropHelpers::AssetList canvasAssets;
if (AssetDropHelpers::AcceptsMimeType(event->mimeData()))
{
AssetDropHelpers::DecodeUiCanvasAssetsFromMimeData(event->mimeData(), canvasAssets);
}
if (!canvasAssets.empty())
{
event->accept();
}
else
{
event->ignore();
}
}
void EditorWindow::dropEvent(QDropEvent* event)
{
AssetDropHelpers::AssetList canvasAssets;
if (AssetDropHelpers::AcceptsMimeType(event->mimeData()))
{
AssetDropHelpers::DecodeUiCanvasAssetsFromMimeData(event->mimeData(), canvasAssets);
}
QStringList canvasFilenames;
for (const AZ::Data::AssetId& canvasAssetId : canvasAssets)
{
const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* source = AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry::GetSourceByUuid(canvasAssetId.m_guid);
if (!source)
{
continue;
}
AZStd::string fullEntryPath = source->GetFullPath();
if (!fullEntryPath.empty())
{
canvasFilenames.push_back(QString(fullEntryPath.c_str()));
}
}
// If in Preview mode, exit back to Edit mode
if (m_editorMode == UiEditorMode::Preview)
{
ToggleEditorMode();
}
OpenCanvases(canvasFilenames);
activateWindow();
m_viewport->setFocus();
}
void EditorWindow::SetupCentralWidget()
{
QWidget* centralWidget = new QWidget(this);
// Create a vertical layout for the central widget that will lay out a tab section widget and a viewport widget
SetupTabbedViewportWidget(centralWidget);
setCentralWidget(centralWidget);
}
void EditorWindow::SetupTabbedViewportWidget(QWidget* parent)
{
// Create a vertical layout for the central widget that will lay out a tab section widget and a viewport widget
QVBoxLayout* tabbedViewportLayout = new QVBoxLayout(parent);
tabbedViewportLayout->setContentsMargins(0, 0, 0, 0);
tabbedViewportLayout->setSpacing(0);
// Create a tab section widget that's a child of the central widget
m_canvasTabSectionWidget = new QWidget(parent);
m_canvasTabSectionWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
// Add the tab section widget to the layout of the central widget
tabbedViewportLayout->addWidget(m_canvasTabSectionWidget);
// Create a horizontal layout for the tab section widget that will lay out a tab bar and an add canvas button
QHBoxLayout* canvasTabSectionWidgetLayout = new QHBoxLayout(m_canvasTabSectionWidget);
canvasTabSectionWidgetLayout->setContentsMargins(0, 0, 0, 0);
// Create a canvas tab bar that's a child of the tab section widget
m_canvasTabWidget = new AzQtComponents::TabWidget(m_canvasTabSectionWidget);
m_canvasTabWidget->tabBar()->setMovable(true);
m_canvasTabWidget->tabBar()->setTabsClosable(true);
m_canvasTabWidget->tabBar()->setExpanding(false);
m_canvasTabWidget->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu);
// Add the canvas tab bar to the layout of the tab section widget
canvasTabSectionWidgetLayout->addWidget(m_canvasTabWidget);
QAction* addCanvasAction = new QAction(QIcon(QStringLiteral(":/stylesheet/img/logging/add-filter.svg")), "", this);
QObject::connect(addCanvasAction, &QAction::triggered, this, [this] { NewCanvas(); });
m_canvasTabWidget->setActionToolBarVisible();
m_canvasTabWidget->addAction(addCanvasAction);
connect(m_canvasTabWidget->tabBar(), &QTabBar::tabCloseRequested, this, &EditorWindow::OnCanvasTabCloseButtonPressed);
connect(m_canvasTabWidget->tabBar(), &QTabBar::currentChanged, this, &EditorWindow::OnCurrentCanvasTabChanged);
connect(m_canvasTabWidget->tabBar(), &QTabBar::customContextMenuRequested, this, &EditorWindow::OnCanvasTabContextMenuRequested);
AzQtComponents::TabWidget::applySecondaryStyle(m_canvasTabWidget, false);
QWidget* viewportWithRulers = m_viewport->CreateViewportWithRulersWidget(this);
// Add the viewport widget to the layout of the central widget
tabbedViewportLayout->addWidget(viewportWithRulers);
}
void EditorWindow::CheckForOrphanedChildren(AZ::EntityId canvasEntityId)
{
bool result = false;
EBUS_EVENT_ID_RESULT(result, canvasEntityId, UiEditorCanvasBus, CheckForOrphanedElements);
if (result)
{
// There are orphaned elements. Ask the user whether to recover or remove them.
int result2 = QMessageBox::warning(this,
tr("Warning: Orphaned Elements"),
tr("This UI canvas has orphaned UI elements that are no longer in the element hierarchy.\n\n"
"They can either be recovered and placed under an element named RecoveredOrphans or they can be deleted.\n\n"
"Do you wish to recover them?"),
(QMessageBox::Yes | QMessageBox::No),
QMessageBox::Yes);
if (result2 == QMessageBox::Yes)
{
EBUS_EVENT_ID(canvasEntityId, UiEditorCanvasBus, RecoverOrphanedElements);
}
else
{
EBUS_EVENT_ID(canvasEntityId, UiEditorCanvasBus, RemoveOrphanedElements);
}
}
}
void EditorWindow::ShowEntitySearchModal()
{
QDialog* dialog = new QDialog(this);
QVBoxLayout* mainLayout = new QVBoxLayout();
mainLayout->setContentsMargins(5, 5, 5, 5);
FindEntityWidget* findEntityWidget = new FindEntityWidget(m_activeCanvasEntityId, dialog);
mainLayout->addWidget(findEntityWidget);
dialog->setWindowTitle(QObject::tr("Find Elements"));
dialog->setMinimumSize(QSize(500, 500));
dialog->resize(QSize(600, 600));
dialog->setLayout(mainLayout);
QWidget::connect(findEntityWidget, &FindEntityWidget::OnFinished, dialog,
[this, dialog](AZStd::vector<AZ::EntityId> selectedEntities)
{
if (!selectedEntities.empty())
{
// Clear any selected entities in the hierarchy so that if an entity is already selected, it will still be scrolled to
m_hierarchy->clearSelection();
m_hierarchy->setCurrentItem(nullptr);
// Expand the entities to be selected in the hierarchy
HierarchyHelpers::ExpandItemsAndAncestors(m_hierarchy, selectedEntities);
// Select the entities in the hierarchy
HierarchyHelpers::SetSelectedItems(m_hierarchy, &selectedEntities);
}
dialog->accept();
}
);
QWidget::connect(findEntityWidget, &FindEntityWidget::OnCanceled, dialog,
[dialog]()
{
dialog->reject();
}
);
dialog->exec();
delete dialog;
}
void EditorWindow::AddTraceMessage(const char *message, AZStd::list<QString>& messageList)
{
messageList.push_back(QString(message));
}
void EditorWindow::ShowTraceMessages(const AZStd::string& canvasName)
{
// Display the errors and warnings in one dialog window
if (m_errors.size() == 0 && m_warnings.size() == 0)
{
return;
}
SandboxEditor::ErrorDialog errorDialog(this);
QString title = QString().asprintf("Error Log - %s", canvasName.c_str());
errorDialog.setWindowTitle(title);
errorDialog.AddMessages(SandboxEditor::ErrorDialog::MessageType::Error, m_errors);
errorDialog.AddMessages(SandboxEditor::ErrorDialog::MessageType::Warning, m_warnings);
errorDialog.exec();
}
void EditorWindow::ClearTraceMessages()
{
m_errors.clear();
m_warnings.clear();
}
#include <moc_EditorWindow.cpp>