// Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "IonQuickAddPanel.h" #include "Cesium3DTileset.h" #include "CesiumCartographicPolygon.h" #include "CesiumEditor.h" #include "CesiumIonClient/Connection.h" #include "CesiumIonRasterOverlay.h" #include "CesiumIonServer.h" #include "CesiumRuntimeSettings.h" #include "CesiumUtility/Uri.h" #include "Editor.h" #include "PropertyCustomizationHelpers.h" #include "SelectCesiumIonToken.h" #include "Styling/SlateStyle.h" #include "Widgets/Images/SThrobber.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SHyperlink.h" #include "Widgets/Layout/SHeader.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Views/SListView.h" using namespace CesiumIonClient; void IonQuickAddPanel::Construct(const FArguments& InArgs) { ChildSlot [SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Top) .AutoHeight() .Padding(FMargin(5.0f, 20.0f, 5.0f, 10.0f)) [SNew(SHeader).Content() [SNew(STextBlock) .TextStyle(FCesiumEditorModule::GetStyle(), "Heading") .Text(InArgs._Title)]] + SVerticalBox::Slot() .VAlign(VAlign_Top) .AutoHeight()[SNew(STextBlock) .Visibility_Lambda([this]() { return this->_message.IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible; }) .Text_Lambda([this]() { return this->_message; }) .AutoWrapText(true)] + SVerticalBox::Slot() .VAlign(VAlign_Top) .Padding(FMargin(5.0f, 0.0f, 5.0f, 20.0f))[this->QuickAddList()]]; } void IonQuickAddPanel::AddItem(const QuickAddItem& item) { _quickAddItems.Add(MakeShared(item)); } void IonQuickAddPanel::ClearItems() { this->_quickAddItems.Empty(); } void IonQuickAddPanel::Refresh() { if (!this->_pQuickAddList) return; this->_pQuickAddList->RequestListRefresh(); } const FText& IonQuickAddPanel::GetMessage() const { return this->_message; } void IonQuickAddPanel::SetMessage(const FText& message) { this->_message = message; } TSharedRef IonQuickAddPanel::QuickAddList() { this->_pQuickAddList = SNew(SListView>) .SelectionMode(ESelectionMode::None) .ListItemsSource(&_quickAddItems) .OnMouseButtonDoubleClick(this, &IonQuickAddPanel::AddItemToLevel) .OnGenerateRow(this, &IonQuickAddPanel::CreateQuickAddItemRow); return this->_pQuickAddList.ToSharedRef(); } TSharedRef IonQuickAddPanel::CreateQuickAddItemRow( TSharedRef item, const TSharedRef& list) { // clang-format off return SNew(STableRow>, list).Content() [ SNew(SBox) .HAlign(EHorizontalAlignment::HAlign_Fill) .HeightOverride(40.0f) .Content() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(5.0f) .VAlign(EVerticalAlignment::VAlign_Center) [ SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(UTF8_TO_TCHAR(item->name.c_str()))) .ToolTipText(FText::FromString(UTF8_TO_TCHAR(item->description.c_str()))) ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(EVerticalAlignment::VAlign_Center) [ PropertyCustomizationHelpers::MakeNewBlueprintButton( FSimpleDelegate::CreateLambda( [this, item]() { this->AddItemToLevel(item); }), FText::FromString( TEXT("Add this item to the level")), TAttribute::Create([this, item]() { return this->_itemsBeingAdded.find(item->name) == this->_itemsBeingAdded.end(); }) ) ] ] ]; // clang-format on } namespace { void showAssetDepotConfirmWindow( const FString& itemName, int64_t missingAsset) { UCesiumIonServer* pServer = FCesiumEditorModule::serverManager().GetCurrentServer(); std::string url = CesiumUtility::Uri::resolve( TCHAR_TO_UTF8(*pServer->ServerUrl), "assetdepot/" + std::to_string(missingAsset)); // clang-format off TSharedRef AssetDepotConfirmWindow = SNew(SWindow) .Title(FText::FromString(TEXT("Asset is not available in My Assets"))) .ClientSize(FVector2D(400.0f, 200.0f)) .Content() [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight().Padding(10.0f) [ SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(TEXT("Before " + itemName + " can be added to your level, it must be added to \"My Assets\" in your Cesium ion account."))) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(EHorizontalAlignment::HAlign_Left) .Padding(10.0f, 5.0f) [ SNew(SHyperlink) .OnNavigate_Lambda([url]() { FPlatformProcess::LaunchURL( UTF8_TO_TCHAR(url.c_str()), NULL, NULL); }) .Text(FText::FromString(TEXT( "Open this asset in the Cesium ion Asset Depot"))) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(EHorizontalAlignment::HAlign_Left) .Padding(10.0f, 5.0f) [ SNew(STextBlock).Text(FText::FromString(TEXT( "Click \"Add to my assets\" in the Cesium ion web page"))) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(EHorizontalAlignment::HAlign_Left) .Padding(10.0f, 5.0f) [ SNew(STextBlock) .Text(FText::FromString(TEXT( "Return to Cesium for Unreal and try adding this asset again"))) ] + SVerticalBox::Slot() .AutoHeight() .HAlign(EHorizontalAlignment::HAlign_Center) .Padding(10.0f, 25.0f) [ SNew(SButton) .OnClicked_Lambda( [&AssetDepotConfirmWindow]() { AssetDepotConfirmWindow ->RequestDestroyWindow(); return FReply::Handled(); }) .Text(FText::FromString(TEXT("Close"))) ] ]; // clang-format on GEditor->EditorAddModalWindow(AssetDepotConfirmWindow); } } // namespace void IonQuickAddPanel::AddIonTilesetToLevel(TSharedRef item) { const std::optional& connection = FCesiumEditorModule::serverManager().GetCurrentSession()->getConnection(); if (!connection) { UE_LOG( LogCesiumEditor, Warning, TEXT("Cannot add an ion asset without an active connection")); return; } std::vector assetIDs{item->tilesetID}; if (item->overlayID > 0) { assetIDs.push_back(item->overlayID); } SelectCesiumIonToken::SelectAndAuthorizeToken( FCesiumEditorModule::serverManager().GetCurrentServer(), assetIDs) .thenInMainThread([connection, tilesetID = item->tilesetID]( const std::optional& /*maybeToken*/) { // If token selection was canceled, or if an error occurred while // selecting the token, ignore it and create the tileset anyway. It's // already been logged if necessary, and we can let the user sort out // the problem using the resulting Troubleshooting panel. return connection->asset(tilesetID); }) .thenInMainThread([item, connection](Response&& response) { if (!response.value.has_value()) { return connection->getAsyncSystem().createResolvedFuture( std::move(int64_t(item->tilesetID))); } if (item->overlayID >= 0) { return connection->asset(item->overlayID) .thenInMainThread([item](Response&& overlayResponse) { return overlayResponse.value.has_value() ? int64_t(-1) : int64_t(item->overlayID); }); } else { return connection->getAsyncSystem().createResolvedFuture(-1); } }) .thenInMainThread([this, item](int64_t missingAsset) { if (missingAsset != -1) { FString itemName(UTF8_TO_TCHAR(item->name.c_str())); showAssetDepotConfirmWindow(itemName, missingAsset); } else { ACesium3DTileset* pTileset = FCesiumEditorModule::FindFirstTilesetWithAssetID(item->tilesetID); if (!pTileset) { pTileset = FCesiumEditorModule::CreateTileset( item->tilesetName, item->tilesetID); } FCesiumEditorModule::serverManager().GetCurrentSession()->getAssets(); if (item->overlayID > 0) { FCesiumEditorModule::AddBaseOverlay( pTileset, item->overlayName, item->overlayID); } pTileset->RerunConstructionScripts(); GEditor->SelectNone(true, false); GEditor->SelectActor(pTileset, true, true, true, true); } this->_itemsBeingAdded.erase(item->name); }); } void IonQuickAddPanel::AddCesiumSunSkyToLevel() { AActor* pActor = FCesiumEditorModule::GetCurrentLevelCesiumSunSky(); if (!pActor) { pActor = FCesiumEditorModule::SpawnCesiumSunSky(); } if (pActor) { GEditor->SelectNone(true, false); GEditor->SelectActor(pActor, true, true, true, true); } } namespace { /** * Set a byte property value in the given object. * * This will search the class of the given object for a property * with the given name, which is assumed to be a byte property, * and assign the given value to this property. * * If the object is `nullptr`, nothing will be done. * If the class does not contain a property with the given name, * or it is not a byte property, a warning will be printed. * * @param object The object to set the value in * @param name The name of the property * @param value The value to set for the property. */ void SetBytePropertyValue( UObjectBase* object, const std::string& name, uint8 value) { if (!object) { return; } UClass* cls = object->GetClass(); FString nameString(name.c_str()); FName propName = FName(*nameString); FProperty* property = cls->FindPropertyByName(propName); if (!property) { UE_LOG( LogCesiumEditor, Warning, TEXT("Property with name %s not found"), *nameString); return; } FByteProperty* byteProperty = CastField(property); if (!byteProperty) { UE_LOG( LogCesiumEditor, Warning, TEXT("Property is not an FByteProperty: %s"), *nameString); return; } byteProperty->SetPropertyValue_InContainer(object, value); } } // namespace void IonQuickAddPanel::AddDynamicPawnToLevel() { AActor* pActor = FCesiumEditorModule::GetCurrentLevelDynamicPawn(); if (!pActor) { pActor = FCesiumEditorModule::SpawnDynamicPawn(); } if (pActor) { uint8 autoPossessValue = static_cast(EAutoReceiveInput::Type::Player0); SetBytePropertyValue(pActor, "AutoPossessPlayer", autoPossessValue); GEditor->SelectNone(true, false); GEditor->SelectActor(pActor, true, true, true, true); } } void IonQuickAddPanel::AddItemToLevel(TSharedRef item) { if (this->_itemsBeingAdded.find(item->name) != this->_itemsBeingAdded.end()) { // Add is already in progress. return; } this->_itemsBeingAdded.insert(item->name); if (item->type == QuickAddItemType::TILESET) { // The blank tileset (identified by the tileset and overlay ID being -1) // can be added directly. All ion tilesets are added via // AddIonTilesetToLevel, which requires an active connection. bool isBlankTileset = item->type == QuickAddItemType::TILESET && item->tilesetID == -1 && item->overlayID == -1; if (isBlankTileset) { FCesiumEditorModule::SpawnBlankTileset(); this->_itemsBeingAdded.erase(item->name); } else { AddIonTilesetToLevel(item); } } else if (item->type == QuickAddItemType::SUNSKY) { AddCesiumSunSkyToLevel(); this->_itemsBeingAdded.erase(item->name); } else if (item->type == QuickAddItemType::DYNAMIC_PAWN) { AddDynamicPawnToLevel(); this->_itemsBeingAdded.erase(item->name); } else if (item->type == QuickAddItemType::CARTOGRAPHIC_POLYGON) { FCesiumEditorModule::SpawnCartographicPolygon(); this->_itemsBeingAdded.erase(item->name); } }