/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Viewport.h" #include "ViewManager.h" #include #include #include #include #include #include #include #include #include namespace AzAssetBrowserRequestHandlerPrivate { using namespace AzToolsFramework; using namespace AzToolsFramework::AssetBrowser; // return true ONLY if we can handle the drop request in the viewport. bool CanSpawnEntityForProduct(const ProductAssetBrowserEntry* product) { if (!product) { return false; } if (product->GetAssetType() == AZ::AzTypeInfo::Uuid()) { return true; // we can always spawn slices. } bool canCreateComponent = false; AZ::AssetTypeInfoBus::EventResult(canCreateComponent, product->GetAssetType(), &AZ::AssetTypeInfo::CanCreateComponent, product->GetAssetId()); if (!canCreateComponent) { return false; } AZ::Uuid componentTypeId = AZ::Uuid::CreateNull(); AZ::AssetTypeInfoBus::EventResult(componentTypeId, product->GetAssetType(), &AZ::AssetTypeInfo::GetComponentTypeId); if (!componentTypeId.IsNull()) { // we have a component type that handles this asset. return true; } // additional operations can be added here. return false; } void SpawnEntityAtPoint(const ProductAssetBrowserEntry* product, AzQtComponents::ViewportDragContext* viewportDragContext, EntityIdList& spawnList, AzFramework::SliceInstantiationTicket& spawnTicket) { // Calculate the drop location. if ((!viewportDragContext) || (!product)) { return; } const AZ::Transform worldTransform = AZ::Transform::CreateTranslation(viewportDragContext->m_hitLocation); // Handle instantiation of slices. if (product->GetAssetType() == AZ::AzTypeInfo::Uuid()) { // Instantiate the slice at the specified location. AZ::Data::Asset asset = AZ::Data::AssetManager::Instance().GetAsset(product->GetAssetId(), false); if (asset) { AzToolsFramework::EditorMetricsEventsBusAction editorMetricsEventsBusActionWrapper(AzToolsFramework::EditorMetricsEventsBusTraits::NavigationTrigger::DragAndDrop); AZStd::string idString; asset.GetId().ToString(idString); AzToolsFramework::EditorMetricsEventsBus::Broadcast(&AzToolsFramework::EditorMetricsEventsBusTraits::SliceInstantiated, AZ::Crc32(idString.c_str())); EditorEntityContextRequestBus::BroadcastResult(spawnTicket, &EditorEntityContextRequests::InstantiateEditorSlice, asset, worldTransform); } } else { ScopedUndoBatch undo("Create entities from asset"); // Add the component(s). AZ::Uuid componentTypeId = AZ::Uuid::CreateNull(); AZ::AssetTypeInfoBus::EventResult(componentTypeId, product->GetAssetType(), &AZ::AssetTypeInfo::GetComponentTypeId); if (!componentTypeId.IsNull()) { AZStd::string entityName; // If the entity is being created from an asset, name it after said asset. const AZ::Data::AssetId assetId = product->GetAssetId(); AZStd::string assetPath; AZ::Data::AssetCatalogRequestBus::BroadcastResult(assetPath, &AZ::Data::AssetCatalogRequests::GetAssetPathById, assetId); if (!assetPath.empty()) { AzFramework::StringFunc::Path::GetFileName(assetPath.c_str(), entityName); } // If not sourced from an asset, generate a generic name. if (entityName.empty()) { entityName = AZStd::string::format("Entity%d", GetIEditor()->GetObjectManager()->GetObjectCount()); } AZ::Entity* newEntity = aznew AZ::Entity(entityName.c_str()); EditorEntityContextRequestBus::Broadcast(&EditorEntityContextRequests::AddRequiredComponents, *newEntity); AzToolsFramework::EditorMetricsEventsBusAction editorMetricsEventsBusActionWrapper(AzToolsFramework::EditorMetricsEventsBusTraits::NavigationTrigger::DragAndDrop); // Create Entity metrics event (Drag+Drop from Asset Browser to Viewport) EditorMetricsEventsBus::Broadcast(&EditorMetricsEventsBusTraits::EntityCreated, newEntity->GetId()); // Create component. AZ::Component* newComponent = newEntity->CreateComponent(componentTypeId); // If it's not an "editor component" then wrap it in a GenericComponentWrapper. bool needsGenericWrapper = azrtti_cast(newComponent) == nullptr; if (needsGenericWrapper) { newEntity->RemoveComponent(newComponent); newComponent = aznew AzToolsFramework::Components::GenericComponentWrapper(newComponent); newEntity->AddComponent(newComponent); } if (newComponent) { // Add Component metrics event (Drag+Drop from Asset Browser to View port to create a new entity with the component) EditorMetricsEventsBus::Broadcast(&EditorMetricsEventsBusTraits::ComponentAdded, newEntity->GetId(), componentTypeId); } // Set entity position. auto* transformComponent = newEntity->FindComponent(); if (transformComponent) { transformComponent->SetWorldTM(worldTransform); } // Add the entity to the editor context, which activates it and creates the sandbox object. EditorEntityContextRequestBus::Broadcast(&EditorEntityContextRequests::AddEditorEntity, newEntity); // set asset after components have been activated in AddEditorEntity method if (newComponent) { Components::EditorComponentBase* asEditorComponent = azrtti_cast(newComponent); if (asEditorComponent) { asEditorComponent->SetPrimaryAsset(assetId); } } // Prepare undo command last so it captures the final state of the entity. EntityCreateCommand* command = aznew EntityCreateCommand(static_cast(newEntity->GetId())); command->Capture(newEntity); command->SetParent(undo.GetUndoBatch()); ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::AddDirtyEntity, newEntity->GetId()); spawnList.push_back(newEntity->GetId()); } } } // Helper utility - determines if the thing being dragged is a FBX from the scene import pipeline // This is important to differentiate. // when someone drags a MTL file directly into the viewport, even from a FBX, we want to spawn it as a decal // but when someone drags a FBX that contains MTL files, we want only to spawn the meshes. // so we have to specifically differentiate here between the mimeData type that contains the source as the root // (dragging the fbx file itself) // and one which contains the actual product at its root. bool IsDragOfFBX(const QMimeData* mimeData) { AZStd::vector entries; if (!AssetBrowserEntry::FromMimeData(mimeData, entries)) { // if mimedata does not even contain entries, no point in proceeding. return false; } for (auto entry : entries) { if (entry->GetEntryType() != AssetBrowserEntry::AssetEntryType::Source) { continue; } // this is a source file. Is it the filetype we're looking for? if (SourceAssetBrowserEntry* source = azrtti_cast(entry)) { if (AzFramework::StringFunc::Equal(source->GetExtension().c_str(), ".fbx", false)) { return true; } } } return false; } } AzAssetBrowserRequestHandler::AzAssetBrowserRequestHandler() : m_previewerFactory(aznew LegacyPreviewerFactory) { using namespace AzToolsFramework::AssetBrowser; AssetBrowserInteractionNotificationBus::Handler::BusConnect(); AzQtComponents::DragAndDropEventsBus::Handler::BusConnect(AzQtComponents::DragAndDropContexts::EditorViewport); AzToolsFramework::AssetBrowser::PreviewerRequestBus::Handler::BusConnect(); } AzAssetBrowserRequestHandler::~AzAssetBrowserRequestHandler() { AzToolsFramework::AssetBrowser::PreviewerRequestBus::Handler::BusDisconnect(); AzToolsFramework::AssetBrowser::AssetBrowserInteractionNotificationBus::Handler::BusDisconnect(); AzQtComponents::DragAndDropEventsBus::Handler::BusDisconnect(); } void AzAssetBrowserRequestHandler::AddContextMenuActions(QWidget* caller, QMenu* menu, const AZStd::vector& entries) { using namespace AzToolsFramework::AssetBrowser; AssetBrowserEntry* entry = entries.empty() ? nullptr : entries.front(); if (!entry) { return; } AZStd::string fullFileDirectory; AZStd::string fullFilePath; AZStd::string fileName; AZStd::string extension; switch (entry->GetEntryType()) { case AssetBrowserEntry::AssetEntryType::Product: // if its a product, we actually want to perform these operations on the source // which will be the parent of the product. entry = entry->GetParent(); if ((!entry) || (entry->GetEntryType() != AssetBrowserEntry::AssetEntryType::Source)) { AZ_Assert(false, "Asset Browser entry product has a non-source parent?"); break; // no valid parent. } // the fall through to the next case is intentional here. case AssetBrowserEntry::AssetEntryType::Source: { AZ::Uuid sourceID = azrtti_cast(entry)->GetSourceUuid(); fullFilePath = entry->GetFullPath(); fullFileDirectory = fullFilePath.substr(0, fullFilePath.find_last_of(AZ_CORRECT_DATABASE_SEPARATOR)); fileName = entry->GetName(); AzFramework::StringFunc::Path::GetExtension(fullFilePath.c_str(), extension); // Add the "Open" menu item. // Note that source file openers are allowed to "veto" the showing of the "Open" menu if it is 100% known that they aren't openable! // for example, custom data formats that are made by Lumberyard that can not have a program associated in the operating system to view them. // If the only opener that can open that file has no m_opener, then it is not openable. SourceFileOpenerList openers; AssetBrowserInteractionNotificationBus::Broadcast(&AssetBrowserInteractionNotificationBus::Events::AddSourceFileOpeners, fullFilePath.c_str(), sourceID, openers); bool validOpenersFound = false; bool vetoOpenerFound = false; for (const SourceFileOpenerDetails& openerDetails : openers) { if (openerDetails.m_opener) { // we found a valid opener (non-null). This means that the system is saying that it knows how to internally // edit this source file and has a custom editor for it. validOpenersFound = true; } else { // if we get here it means someone intentionally registered a callback with a null function pointer // the API treats this as a 'veto' opener - meaning that the system wants us NOT to allow the operating system // to open this source file as a default fallback. vetoOpenerFound = true; } } if (validOpenersFound) { // if we get here then there is an opener installed for this kind of asset // and it is not null, meaning that it is not vetoing our ability to open the file. for (const SourceFileOpenerDetails& openerDetails : openers) { // bind that function to the current loop element. if (openerDetails.m_opener) // only VALID openers with an actual callback. { menu->addAction(openerDetails.m_iconToUse, QObject::tr(openerDetails.m_displayText.c_str()), [sourceID, fullFilePath, openerDetails]() { openerDetails.m_opener(fullFilePath.c_str(), sourceID); }); } } } // we always add the default "open with your operating system" unless a veto opener is found if (!vetoOpenerFound) { // if we found no valid openers and no veto openers then just allow it to be opened with the operating system itself. menu->addAction(QObject::tr("Open with associated application..."), [this, fullFilePath]() { OpenWithOS(fullFilePath); }); } AZStd::vector products; entry->GetChildrenRecursively(products); // slice source files need to react by adding additional menu items, regardless of status of compile or presence of products. if (AzFramework::StringFunc::Equal(extension.c_str(), AzToolsFramework::SliceUtilities::GetSliceFileExtension().c_str(), false)) { AzToolsFramework::SliceUtilities::CreateSliceAssetContextMenu(menu, fullFilePath); // SliceUtilities is in AZToolsFramework and can't open viewports, so add the relationship view open command here. if (!products.empty()) { const ProductAssetBrowserEntry* productEntry = products[0]; menu->addAction("Open in Slice Relationship View", [productEntry]() { QtViewPaneManager::instance()->OpenPane(LyViewPane::SliceRelationships); const ProductAssetBrowserEntry* product = azrtti_cast(productEntry); AzToolsFramework::SliceRelationshipRequestBus::Broadcast(&AzToolsFramework::SliceRelationshipRequests::OnSliceRelationshipViewRequested, product->GetAssetId()); }); } } else if (AzFramework::StringFunc::Equal(extension.c_str(), AzToolsFramework::Layers::EditorLayerComponent::GetLayerExtensionWithDot().c_str(), false)) { QString levelPath = Path::GetPath(GetIEditor()->GetDocument()->GetActivePathName()); AzToolsFramework::Layers::EditorLayerComponent::CreateLayerAssetContextMenu(menu, fullFilePath, levelPath); } if (products.empty()) { if (entry->GetEntryType() == AssetBrowserEntry::AssetEntryType::Source) { CFileUtil::PopulateQMenu(caller, menu, fileName.c_str(), fullFileDirectory.c_str()); } return; } CFileUtil::PopulateQMenu(caller, menu, fileName.c_str(), fullFileDirectory.c_str()); } break; case AssetBrowserEntry::AssetEntryType::Folder: { fullFileDirectory = entry->GetFullPath(); // we are sending an empty filename to indicate that it is a folder and not a file CFileUtil::PopulateQMenu(caller, menu, fileName.c_str(), fullFileDirectory.c_str()); } break; default: break; } } bool AzAssetBrowserRequestHandler::CanAcceptDragAndDropEvent(QDropEvent* event, AzQtComponents::DragAndDropContextBase& context) const { using namespace AzQtComponents; using namespace AzToolsFramework; using namespace AzToolsFramework::AssetBrowser; using namespace AzAssetBrowserRequestHandlerPrivate; // if a listener with a higher priority already claimed this event, do not touch it. ViewportDragContext* viewportDragContext = azrtti_cast(&context); if ((!event) || (!event->mimeData()) || (event->isAccepted()) || (!viewportDragContext)) { return false; } // is it something we know how to spawn? bool canSpawn = false; AzToolsFramework::AssetBrowser::AssetBrowserEntry::ForEachEntryInMimeData(event->mimeData(), [&](const AzToolsFramework::AssetBrowser::ProductAssetBrowserEntry* product) { if (CanSpawnEntityForProduct(product)) { canSpawn = true; } }); return canSpawn; } void AzAssetBrowserRequestHandler::DragEnter(QDragEnterEvent* event, AzQtComponents::DragAndDropContextBase& context) { if (CanAcceptDragAndDropEvent(event, context)) { event->setDropAction(Qt::CopyAction); event->setAccepted(true); } } void AzAssetBrowserRequestHandler::DragMove(QDragMoveEvent* event, AzQtComponents::DragAndDropContextBase& context) { if (CanAcceptDragAndDropEvent(event, context)) { event->setDropAction(Qt::CopyAction); event->setAccepted(true); } } void AzAssetBrowserRequestHandler::DragLeave(QDragLeaveEvent* /*event*/) { // opportunities to show ghosted entities or previews here. } void AzAssetBrowserRequestHandler::Drop(QDropEvent* event, AzQtComponents::DragAndDropContextBase& context) { using namespace AzToolsFramework; using namespace AzToolsFramework::AssetBrowser; using namespace AzQtComponents; using namespace AzAssetBrowserRequestHandlerPrivate; // ALWAYS CHECK - you are not the only one connected to this bus, and someone else may have already // handled the event or accepted the drop - it might not contain types relevant to you. // you still get informed about the drop event in case you did some stuff in your gui and need to clean it up. if (!CanAcceptDragAndDropEvent(event, context)) { return; } // we wouldn't reach this code if the following cast is null or the event was null or accepted was already true. ViewportDragContext* viewportDragContext = azrtti_cast(&context); event->setDropAction(Qt::CopyAction); event->setAccepted(true); // spawn entities! EntityIdList spawnedEntities; AzFramework::SliceInstantiationTicket spawnTicket; // make a scoped undo that covers the ENTIRE operation. ScopedUndoBatch undo("Create entities from asset"); AssetBrowserEntry::ForEachEntryInMimeData(event->mimeData(), [&](const ProductAssetBrowserEntry* product) { if (CanSpawnEntityForProduct(product)) { SpawnEntityAtPoint(product, viewportDragContext, spawnedEntities, spawnTicket); } }); // Select the new entity (and deselect others). if (!spawnedEntities.empty()) { ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::SetSelectedEntities, spawnedEntities); } } const AzToolsFramework::AssetBrowser::PreviewerFactory* AzAssetBrowserRequestHandler::GetPreviewerFactory(const AzToolsFramework::AssetBrowser::AssetBrowserEntry* entry) const { if (m_previewerFactory->IsEntrySupported(entry)) { return m_previewerFactory.get(); } return nullptr; } void AzAssetBrowserRequestHandler::AddSourceFileOpeners(const char* fullSourceFileName, const AZ::Uuid& sourceUUID, AzToolsFramework::AssetBrowser::SourceFileOpenerList& openers) { using namespace AzToolsFramework; //Get asset group to support a variety of file extensions const AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry* fullDetails = AzToolsFramework::AssetBrowser::SourceAssetBrowserEntry::GetSourceByUuid(sourceUUID); if (!fullDetails) { return; } QString assetGroup; AZ::AssetTypeInfoBus::EventResult(assetGroup, fullDetails->GetPrimaryAssetType(), &AZ::AssetTypeInfo::GetGroup); if (AZStd::wildcard_match("*.lua", fullSourceFileName)) { AZStd::string fullName(fullSourceFileName); // LUA files can be opened with the lumberyard LUA editor. openers.push_back( { "Lumberyard_LUA_Editor", "Open in Lumberyard LUA Editor...", QIcon(), [](const char* fullSourceFileNameInCallback, const AZ::Uuid& /*sourceUUID*/) { // we know how to handle LUA files (open with the lua Editor. EditorRequestBus::Broadcast(&EditorRequests::LaunchLuaEditor, fullSourceFileNameInCallback); } }); } if (!openers.empty()) { return; // we found one } // if we still havent found one, check to see if it is a default "generic" serializable asset // and open the asset editor if so. Check whether the Generic Asset handler handles this kind of asset. // to do so we need the actual type of that asset, which requires an asset type, not a source type. AZ::Data::AssetManager& manager = AZ::Data::AssetManager::Instance(); // find a product type to query against. AZStd::vector candidates; fullDetails->GetChildrenRecursively(candidates); // find the first one that is handled by something: for (const AssetBrowser::ProductAssetBrowserEntry* productEntry : candidates) { // is there a Generic Asset Handler for it? AZ::Data::AssetType productAssetType = productEntry->GetAssetType(); if ((productAssetType == AZ::Data::s_invalidAssetType) || (!productEntry->GetAssetId().IsValid())) { continue; } if (const AZ::Data::AssetHandler* assetHandler = manager.GetHandler(productAssetType)) { if (!azrtti_istypeof(assetHandler)) { // it is not the generic asset handler. continue; } // yes, it is the generic asset handler, so install an opener that sends it to the Asset Editor. AZ::Data::AssetId assetId = productEntry->GetAssetId(); AZ::Data::AssetType assetType = productEntry->GetAssetType(); openers.push_back( { "Open_In_Asset_Editor", "Open in Asset Editor...", QIcon(), [assetId, assetType](const char* /*fullSourceFileNameInCallback*/, const AZ::Uuid& /*sourceUUID*/) { AZ::Data::Asset asset = AZ::Data::AssetManager::Instance().GetAsset(assetId, assetType, false); AzToolsFramework::AssetEditor::AssetEditorRequestsBus::Broadcast(&AzToolsFramework::AssetEditor::AssetEditorRequests::OpenAssetEditor, asset); } }); break; // no need to proceed further } } } void AzAssetBrowserRequestHandler::OpenAssetInAssociatedEditor(const AZ::Data::AssetId& assetId, bool& alreadyHandled) { using namespace AzToolsFramework::AssetBrowser; if (alreadyHandled) { // a higher priority listener has already taken this request. return; } const SourceAssetBrowserEntry* source = SourceAssetBrowserEntry::GetSourceByUuid(assetId.m_guid); if (!source) { return; } AZStd::string fullEntryPath = source->GetFullPath(); AZ::Uuid sourceID = source->GetSourceUuid(); if (fullEntryPath.empty()) { return; } QWidget* mainWindow = nullptr; AzToolsFramework::EditorRequestBus::BroadcastResult(mainWindow, &AzToolsFramework::EditorRequests::GetMainWindow); SourceFileOpenerList openers; AssetBrowserInteractionNotificationBus::Broadcast(&AssetBrowserInteractionNotificationBus::Events::AddSourceFileOpeners, fullEntryPath.c_str(), sourceID, openers); // did anyone actually accept it? if (!openers.empty()) { // yes, call the opener and return. // are there more than one opener(s)? const SourceFileOpenerDetails* openerToUse = nullptr; // a function which reassigns openerToUse to be the selected one. AZStd::function switchToOpener = [&openerToUse](const SourceFileOpenerDetails* switchTo) { openerToUse = switchTo; }; // callers are allowed to add nullptr to openers. So we only evaluate the valid ones. // and if there is only one valid one, we use that one. const SourceFileOpenerDetails* firstValidOpener = nullptr; int numValidOpeners = 0; QMenu menu(mainWindow); for (const SourceFileOpenerDetails& openerDetails : openers) { // bind that function to the current loop element. if (openerDetails.m_opener) // only VALID openers with an actual callback. { ++numValidOpeners; if (!firstValidOpener) { firstValidOpener = &openerDetails; } // bind a callback such that when the menu item is clicked, it sets that as the opener to use. menu.addAction(openerDetails.m_iconToUse, QObject::tr(openerDetails.m_displayText.c_str()), mainWindow, AZStd::bind(switchToOpener, &openerDetails)); } } if (numValidOpeners > 1) // more than one option was added { menu.addSeparator(); menu.addAction(QObject::tr("Cancel"), AZStd::bind(switchToOpener, nullptr)); // just something to click on to avoid doing anything. menu.exec(QCursor::pos()); } else if (numValidOpeners == 1) { openerToUse = firstValidOpener; } // did we select one and did it have a function to call? if ((openerToUse) && (openerToUse->m_opener)) { openerToUse->m_opener(fullEntryPath.c_str(), sourceID); } alreadyHandled = true; return; // an opener handled this, no need to proceed further. } // if we get here, nothing handled it, so try the operating system. alreadyHandled = OpenWithOS(fullEntryPath); } bool AzAssetBrowserRequestHandler::OpenWithOS(const AZStd::string& fullEntryPath) { bool openedSuccessfully = QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromUtf8(fullEntryPath.c_str()))); if (!openedSuccessfully) { AZ_Printf("Asset Browser", "Unable to open '%s' using the operating system. There might be no editor associated with this kind of file.\n", fullEntryPath.c_str()); } return openedSuccessfully; }