/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "stdafx.h" #include "EditorCommon.h" #include "CanvasHelpers.h" #include "AssetDropHelpers.h" #include #include #include #include #include #include #include #include #include #include #include #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" #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" 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_prefabFiles() , m_actionsEnabledWithSelection() , m_pasteAsSiblingAction(nullptr) , m_pasteAsChildAction(nullptr) , m_actionsEnabledWithAlignAllowed() , m_previewModeCanvasEntityId() , m_previewModeCanvasSize(0.0f, 0.0f) , m_clipboardConnection() , m_newCanvasCount(1) { // 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); UpdatePrefabFiles(); // 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, const AZ::SliceComponent::SliceInstanceAddress& sliceAddress, 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, 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) { // Submit metrics for a canvas that has changed since it was last // loaded/created, and all changes have been saved. if (canvasMetadata.m_canvasChangedAndSaved && !GetChangesHaveBeenMade(canvasMetadata)) { SubmitUnloadSavedCanvasMetricEvent(canvasMetadata.m_canvasEntityId); } gEnv->pLyShine->ReleaseCanvas(canvasMetadata.m_canvasEntityId, true); } bool EditorWindow::IsCanvasTabMetadataValidForTabIndex(int index) { QVariant data = m_canvasTabWidget->tabBar()->tabData(index); return data.isValid(); } AZ::EntityId EditorWindow::GetCanvasEntityIdForTabIndex(int index) { QVariant data = m_canvasTabWidget->tabBar()->tabData(index); AZ_Assert(data.isValid(), "Canvas tab metadata is not valid"); if (data.isValid()) { auto canvasTabMetadata = data.value(); 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(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); 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::EntityIdContextQueryBus::EventResult(sliceAddress, sliceEntityId, &AzFramework::EntityIdContextQueries::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 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 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 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& 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(); m_viewport->UpdateViewportBackground(); 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(m_undoGroup->activeStack()); } AssetTreeEntry* EditorWindow::GetSliceLibraryTree() { if (!m_sliceLibraryTree) { const AZStd::string pathToSearch("ui/slices/library/"); const AZ::Data::AssetType sliceAssetType(AZ::AzTypeInfo::Uuid()); m_sliceLibraryTree = AssetTreeEntry::BuildAssetTree(sliceAssetType, pathToSearch); } return m_sliceLibraryTree; } void EditorWindow::UpdatePrefabFiles() { m_prefabFiles.clear(); // IMPORTANT: ScanDirectory() is VERY slow. It can easily take as much // as a whole second to execute. That's why we want to cache its result // up front and ONLY access the cached data. GetIEditor()->GetFileUtil()->ScanDirectory("", "*." UICANVASEDITOR_PREFAB_EXTENSION, m_prefabFiles); SortPrefabsList(); } IFileUtil::FileArray& EditorWindow::GetPrefabFiles() { return m_prefabFiles; } void EditorWindow::AddPrefabFile(const QString& prefabFilename) { IFileUtil::FileDesc fd; fd.filename = prefabFilename; m_prefabFiles.push_back(fd); SortPrefabsList(); } void EditorWindow::SortPrefabsList() { AZStd::sort(m_prefabFiles.begin(), m_prefabFiles.end(), [](const IFileUtil::FileDesc& fd1, const IFileUtil::FileDesc& fd2) { // Some of the files in the list are in different directories, so we // explicitly sort by filename only. AZStd::string fd1Filename; AzFramework::StringFunc::Path::GetFileName(fd1.filename.toUtf8().data(), fd1Filename); AZStd::string fd2Filename; AzFramework::StringFunc::Path::GetFileName(fd2.filename.toUtf8().data(), fd2Filename); return fd1Filename < fd2Filename; }); } 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(m_viewport->size().width()), aznumeric_cast(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 list = findChildren(); 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 list = findChildren(); 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 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) { // 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& asset) { AZ::EntityId canvasToUpdate = GetCanvasForEntityContext(contextId); UiCanvasMetadata* canvasMetadata = GetCanvasMetadata(canvasToUpdate); if (canvasMetadata->m_isSliceEditing && asset.GetType() == AZ::AzTypeInfo::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::EntityIdContextQueryBus::EventResult(sliceAddress, canvasMetadata->m_sliceEntityId, &AzFramework::EntityIdContextQueries::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](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"; void EditorWindow::SubmitUnloadSavedCanvasMetricEvent(AZ::EntityId canvasEntityId) { // Create an unload canvas event auto eventId = LyMetrics_CreateEvent(UIEDITOR_UNLOAD_SAVED_CANVAS_METRIC_EVENT_NAME); // Add unique canvas Id attribute AZ::u64 uniqueId = 0; EBUS_EVENT_ID_RESULT(uniqueId, canvasEntityId, UiCanvasBus, GetUniqueCanvasId); QString uniqueIdString; uniqueIdString.setNum(uniqueId); LyMetrics_AddAttribute(eventId, UIEDITOR_CANVAS_ID_ATTRIBUTE_NAME, qPrintable(uniqueIdString)); // Add canvas size metric AZ::Vector2 canvasSize; EBUS_EVENT_ID_RESULT(canvasSize, canvasEntityId, UiCanvasBus, GetCanvasSize); LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_WIDTH_METRIC_NAME, canvasSize.GetX()); LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_HEIGHT_METRIC_NAME, canvasSize.GetY()); // Add max hierarchy depth metric LyShine::EntityArray childElements; EBUS_EVENT_ID_RESULT(childElements, canvasEntityId, UiCanvasBus, GetChildElements); int maxDepth = GetCanvasMaxHierarchyDepth(childElements); LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_MAX_HIERARCHY_DEPTH_METRIC_NAME, maxDepth); // Get a list of the component types that can be added to a UI element // The ComponentTypeData struct has a flag to say whether the component is an LyShine component AZStd::vector uiComponentTypes = ComponentHelpers::GetAllComponentTypesThatCanAppearInAddComponentMenu(); // Make a list of all elements of this canvas LyShine::EntityArray allElements; EBUS_EVENT_ID(canvasEntityId, UiCanvasBus, FindElements, [](const AZ::Entity* entity) { return true; }, allElements); // Add total number of elements metric LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_ELEMENT_METRIC_NAME, allElements.size()); std::vector numElementsWithComponent(uiComponentTypes.size(), 0); int numElementsWithCustomComponent = 0; int numCustomComponentsAvailable = 0; for (int i = 0; i < uiComponentTypes.size(); i++) { if (!uiComponentTypes[i].isLyShineComponent) { ++numCustomComponentsAvailable; } } std::vector numElementsWithAnchorPreset(AnchorPresets::PresetIndexCount, 0); int numElementsWithCustomAnchors = 0; std::vector numElementsWithPivotPreset(PivotPresets::PresetIndexCount, 0); int numElementsWithCustomPivot = 0; int numRotatedElements = 0; int numScaledElements = 0; int numScaleToDeviceElements = 0; for (auto entity : allElements) { // Check which components this element has bool elementHasCustomComponent = false; for (int i = 0; i < uiComponentTypes.size(); i++) { if (entity->FindComponent(uiComponentTypes[i].classData->m_typeId)) { numElementsWithComponent[i]++; if (!uiComponentTypes[i].isLyShineComponent) { elementHasCustomComponent = true; } } } if (elementHasCustomComponent) { numElementsWithCustomComponent++; } // Check if this element is controlled by its parent bool isControlledByParent = false; AZ::Entity* parentElement = EntityHelpers::GetParentElement(entity); if (parentElement) { EBUS_EVENT_ID_RESULT(isControlledByParent, parentElement->GetId(), UiLayoutBus, IsControllingChild, entity->GetId()); } if (!isControlledByParent) { // Check if this element is scaled AZ::Vector2 scale(1.0f, 1.0f); EBUS_EVENT_ID_RESULT(scale, entity->GetId(), UiTransformBus, GetScale); if (scale.GetX() != 1.0f || scale.GetY() != 1.0f) { numScaledElements++; } // Check if this element is rotated float rotation = 0.0f; EBUS_EVENT_ID_RESULT(rotation, entity->GetId(), UiTransformBus, GetZRotation); if (rotation != 0.0f) { numRotatedElements++; } // Check if this element is using an anchor preset UiTransform2dInterface::Anchors anchors; EBUS_EVENT_ID_RESULT(anchors, entity->GetId(), UiTransform2dBus, GetAnchors); AZ::Vector4 anchorVect(anchors.m_left, anchors.m_top, anchors.m_right, anchors.m_bottom); int anchorPresetIndex = AnchorPresets::AnchorToPresetIndex(anchorVect); if (anchorPresetIndex >= 0) { numElementsWithAnchorPreset[anchorPresetIndex]++; } else { numElementsWithCustomAnchors++; } // Check if this element is using a pivot preset AZ::Vector2 pivot; EBUS_EVENT_ID_RESULT(pivot, entity->GetId(), UiTransformBus, GetPivot); AZ::Vector2 pivotVect(pivot.GetX(), pivot.GetY()); int pivotPresetIndex = PivotPresets::PivotToPresetIndex(pivotVect); if (pivotPresetIndex >= 0) { numElementsWithPivotPreset[pivotPresetIndex]++; } else { numElementsWithCustomPivot++; } } // Check if this element is scaled to device UiTransformInterface::ScaleToDeviceMode scaleToDeviceMode = UiTransformInterface::ScaleToDeviceMode::None; EBUS_EVENT_ID_RESULT(scaleToDeviceMode, entity->GetId(), UiTransformBus, GetScaleToDeviceMode); if (scaleToDeviceMode != UiTransformInterface::ScaleToDeviceMode::None) { numScaleToDeviceElements++; } } // Add metric for each internal component representing the number of elements having that component int numCustomComponentsUsed = 0; for (int i = 0; i < uiComponentTypes.size(); i++) { auto& componentType = uiComponentTypes[i]; if (componentType.isLyShineComponent) { AZ::Edit::ClassData* editInfo = componentType.classData->m_editData; if (editInfo) { int count = numElementsWithComponent[i]; AZStd::string metricName(UIEDITOR_CANVAS_NUM_ELEMENTS_WITH_COMPONENT_PREFIX_METRIC_NAME); metricName += editInfo->m_name; LyMetrics_AddMetric(eventId, metricName.c_str(), count); } } else { numCustomComponentsUsed++; } } // Add metric for the number of elements that have a custom component LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_ELEMENTS_WITH_CUSTOM_COMPONENT_METRIC_NAME, numElementsWithCustomComponent); // Add metric for the number of unique custom components used LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_UNIQUE_CUSTOM_COMPONENT_NAME, numCustomComponentsUsed); // Add a metric for the number of available custom components LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_AVAILABLE_CUSTOM_COMPONENT_NAME, numCustomComponentsAvailable); // Construct a string representing the number of elements that use each anchor preset e.g. {0, 1, 6, 20, ...} QString anchorPresetString("{"); for (int i = 0; i < AnchorPresets::PresetIndexCount; i++) { anchorPresetString.append(QString::number(numElementsWithAnchorPreset[i])); if (i < AnchorPresets::PresetIndexCount - 1) { anchorPresetString.append(", "); } } anchorPresetString.append("}"); // Add attribute representing the number of elements using each anchor preset LyMetrics_AddAttribute(eventId, UIEDITOR_CANVAS_NUM_ANCHOR_PRESETS_ATTRIBUTE_NAME, qPrintable(anchorPresetString)); // Add metric representing the number of elements with their anchors set to a custom value LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_ANCHOR_CUSTOM_ATTRIBUTE_NAME, numElementsWithCustomAnchors); // Construct a string representing the number of elements that use each pivot preset e.g. {0, 1, 6, 20, ...} QString pivotPresetString("{"); for (int i = 0; i < PivotPresets::PresetIndexCount; i++) { pivotPresetString.append(QString::number(numElementsWithPivotPreset[i])); if (i < PivotPresets::PresetIndexCount - 1) { pivotPresetString.append(", "); } } pivotPresetString.append("}"); // Add attribute representing the number of elements using each pivot preset LyMetrics_AddAttribute(eventId, UIEDITOR_CANVAS_NUM_PIVOT_PRESETS_ATTRIBUTE_NAME, qPrintable(pivotPresetString)); // Add metric representing the number of elements with their pivot set to a custom value LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_PIVOT_CUSTOM_ATTRIBUTE_NAME, numElementsWithCustomPivot); // Add number of rotated elements metric LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_ROTATED_ELEMENT_METRIC_NAME, numRotatedElements); // Add number of scaled elements metric LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_SCALED_ELEMENT_METRIC_NAME, numScaledElements); // Add metric representing the number of elements that are scaled to device LyMetrics_AddMetric(eventId, UIEDITOR_CANVAS_NUM_SCALE_TO_DEVICE_ELEMENT_METRIC_NAME, numScaleToDeviceElements); // Submit the event LyMetrics_SubmitEvent(eventId); } int EditorWindow::GetCanvasMaxHierarchyDepth(const LyShine::EntityArray& rootChildElements) { int depth = 0; if (rootChildElements.empty()) { return depth; } int numChildrenCurLevel = rootChildElements.size(); int numChildrenNextLevel = 0; std::list 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 result = 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 (result == 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 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& 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