// Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "SelectCesiumIonToken.h" #include "Cesium3DTileset.h" #include "CesiumEditor.h" #include "CesiumIonRasterOverlay.h" #include "CesiumIonServerDisplay.h" #include "CesiumRuntime.h" #include "CesiumRuntimeSettings.h" #include "CesiumSourceControl.h" #include "CesiumUtility/joinToString.h" #include "Editor.h" #include "EditorStyleSet.h" #include "EngineUtils.h" #include "Framework/Application/SlateApplication.h" #include "Misc/App.h" #include "PropertyCustomizationHelpers.h" #include "Runtime/Launch/Resources/Version.h" #include "ScopedTransaction.h" #include "Widgets/Images/SThrobber.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Text/STextBlock.h" using namespace CesiumAsync; using namespace CesiumIonClient; using namespace CesiumUtility; /*static*/ TSharedPtr SelectCesiumIonToken::_pExistingPanel{}; /*static*/ SharedFuture> SelectCesiumIonToken::SelectNewToken(UCesiumIonServer* pServer) { std::shared_ptr pSession = FCesiumEditorModule::serverManager().GetSession(pServer); // If the current server doesn't require tokens, don't bother opening the // window to create one. if (!pSession->isAuthenticationRequired()) { return getAsyncSystem() .createResolvedFuture(std::optional(Token())) .share(); } if (SelectCesiumIonToken::_pExistingPanel.IsValid()) { SelectCesiumIonToken::_pExistingPanel->BringToFront(); } else { TSharedRef Panel = SNew(SelectCesiumIonToken).Server(pServer); SelectCesiumIonToken::_pExistingPanel = Panel; Panel->_promise = getAsyncSystem().createPromise>(); Panel->_future = Panel->_promise->getFuture().share(); Panel->GetOnWindowClosedEvent().AddLambda( [Panel](const TSharedRef& pWindow) { if (Panel->_promise) { // Promise is still outstanding, so resolve it now (no token was // selected). Panel->_promise->resolve(std::nullopt); } SelectCesiumIonToken::_pExistingPanel.Reset(); }); FSlateApplication::Get().AddWindow(Panel); } return *SelectCesiumIonToken::_pExistingPanel->_future; } Future> SelectCesiumIonToken::SelectTokenIfNecessary(UCesiumIonServer* pServer) { return FCesiumEditorModule::serverManager() .GetSession(pServer) ->getProjectDefaultTokenDetails() .thenInMainThread([pServer](const Token& token) { if (token.token.empty()) { return SelectCesiumIonToken::SelectNewToken(pServer).thenImmediately( [](const std::optional& maybeToken) { return maybeToken; }); } else { return getAsyncSystem().createResolvedFuture( std::make_optional(token)); } }); } namespace { std::vector findUnauthorizedAssets( const std::vector& authorizedAssets, const std::vector& requiredAssets) { std::vector missingAssets; for (int64_t assetID : requiredAssets) { auto it = std::find(authorizedAssets.begin(), authorizedAssets.end(), assetID); if (it == authorizedAssets.end()) { missingAssets.emplace_back(assetID); } } return missingAssets; } } // namespace Future> SelectCesiumIonToken::SelectAndAuthorizeToken( UCesiumIonServer* pServer, const std::vector& assetIDs) { std::shared_ptr pSession = FCesiumEditorModule::serverManager().GetSession(pServer); // If the current server doesn't require tokens, don't try to create or // authorize one. if (!pSession->isAuthenticationRequired()) { return getAsyncSystem().createResolvedFuture(std::optional(Token())); } return SelectTokenIfNecessary(pServer).thenInMainThread([pSession, assetIDs]( const std::optional< Token>& maybeToken) { const std::optional& maybeConnection = pSession->getConnection(); if (maybeConnection && maybeToken && !maybeToken->id.empty() && maybeToken->assetIds) { std::vector missingAssets = findUnauthorizedAssets(*maybeToken->assetIds, assetIDs); if (!missingAssets.empty()) { // Refresh the token details. We don't want to update the token based // on stale information. return maybeConnection->token(maybeToken->id) .thenInMainThread([pSession, maybeToken, assetIDs]( Response&& response) { if (response.value) { std::vector missingAssets = findUnauthorizedAssets(*maybeToken->assetIds, assetIDs); if (!missingAssets.empty()) { std::vector idStrings(missingAssets.size()); std::transform( missingAssets.begin(), missingAssets.end(), idStrings.begin(), [](int64_t id) { return std::to_string(id); }); UE_LOG( LogCesiumEditor, Warning, TEXT( "Authorizing the project's default Cesium ion token to access the following asset IDs: %s"), UTF8_TO_TCHAR(joinToString(idStrings, ", ").c_str())); Token newToken = *maybeToken; size_t destinationIndex = newToken.assetIds->size(); newToken.assetIds->resize( newToken.assetIds->size() + missingAssets.size()); std::copy( missingAssets.begin(), missingAssets.end(), newToken.assetIds->begin() + destinationIndex); return pSession->getConnection() ->modifyToken( newToken.id, newToken.name, newToken.assetIds, newToken.scopes, newToken.allowedUrls) .thenImmediately([maybeToken](Response&&) { return maybeToken; }); } } return getAsyncSystem().createResolvedFuture( std::optional(maybeToken)); }); } } return getAsyncSystem().createResolvedFuture( std::optional(maybeToken)); }); } void SelectCesiumIonToken::Construct(const FArguments& InArgs) { UCesiumIonServer* pServer = InArgs._Server; if (this->_pServer.IsValid() && this->_tokensUpdatedDelegateHandle.IsValid()) { FCesiumEditorModule::serverManager() .GetSession(this->_pServer.Get()) ->TokensUpdated.Remove(this->_tokensUpdatedDelegateHandle); } this->_pServer = pServer; std::shared_ptr pSession = FCesiumEditorModule::serverManager().GetSession(this->_pServer.Get()); this->_tokensUpdatedDelegateHandle = pSession->TokensUpdated.AddRaw( this, &SelectCesiumIonToken::RefreshTokens); TSharedRef pLoaderOrContent = SNew(SVerticalBox).Visibility_Lambda([pSession]() { return pSession->getAppData().needsOauthAuthentication() ? EVisibility::Visible : EVisibility::Collapsed; }); TSharedRef pSingleUserText = SNew(SVerticalBox).Visibility_Lambda([pSession]() { return !pSession->getAppData().needsOauthAuthentication() ? EVisibility::Visible : EVisibility::Collapsed; }); pSingleUserText->AddSlot().AutoHeight() [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(TEXT( "Cesium for Unreal is currently connected to a Cesium ion server running in single-user authentication mode. Tokens are not used in this mode.")))]; pLoaderOrContent->AddSlot().AutoHeight() [SNew(STextBlock) .AutoWrapText(true) .Text(FText::FromString(TEXT( "Cesium for Unreal embeds a Cesium ion token in your project in order to allow it to access the assets you add to your levels. Select the Cesium ion token to use.")))]; pLoaderOrContent->AddSlot().AutoHeight().Padding( 5.0f)[SNew(CesiumIonServerDisplay).Server(pServer)]; pLoaderOrContent->AddSlot() .Padding(0.0f, 10.0f, 0.0, 10.0f) .AutoHeight() [SNew(STextBlock) .Visibility_Lambda([pSession]() { return pSession->isConnected() ? EVisibility::Collapsed : EVisibility::Visible; }) .AutoWrapText(true) .Text(FText::FromString(TEXT( "Please connect to Cesium ion to select a token from your account or to create a new token.")))]; pLoaderOrContent->AddSlot() .AutoHeight()[SNew(SThrobber).Visibility_Lambda([pSession]() { return pSession->isLoadingTokenList() ? EVisibility::Visible : EVisibility::Collapsed; })]; TSharedRef pMainVerticalBox = SNew(SVerticalBox).Visibility_Lambda([pSession]() { return pSession->isLoadingTokenList() ? EVisibility::Collapsed : EVisibility::Visible; }); pLoaderOrContent->AddSlot().AutoHeight()[pMainVerticalBox]; this->_createNewToken.name = FString(FApp::GetProjectName()) + TEXT(" (Created by Cesium for Unreal)"); this->_useExistingToken.token.id = TCHAR_TO_UTF8(*pServer->DefaultIonAccessTokenId); this->_useExistingToken.token.token = TCHAR_TO_UTF8(*pServer->DefaultIonAccessToken); this->_specifyToken.token = pServer->DefaultIonAccessToken; this->_tokenSource = pServer->DefaultIonAccessToken.IsEmpty() && pSession->isConnected() ? TokenSource::Create : TokenSource::Specify; this->createRadioButton( pSession, pMainVerticalBox, this->_tokenSource, TokenSource::Create, TEXT("Create a new token"), true, SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(EVerticalAlignment::VAlign_Center) .AutoWidth() .Padding(5.0f)[SNew(STextBlock) .Text(FText::FromString(TEXT("Name:")))] + SHorizontalBox::Slot() .VAlign(EVerticalAlignment::VAlign_Center) .AutoWidth() .MaxWidth(500.0f) .Padding( 5.0f)[SNew(SEditableTextBox) .Text(this, &SelectCesiumIonToken::GetNewTokenName) .MinDesiredWidth(200.0f) .OnTextChanged( this, &SelectCesiumIonToken::SetNewTokenName)]); SAssignNew(this->_pTokensCombo, SComboBox>) .OptionsSource(&this->_tokens) .OnGenerateWidget( this, &SelectCesiumIonToken::OnGenerateTokenComboBoxEntry) .OnSelectionChanged(this, &SelectCesiumIonToken::OnSelectExistingToken) .Content()[SNew(STextBlock).MinDesiredWidth(200.0f).Text_Lambda([this]() { return this->_pTokensCombo.IsValid() && this->_pTokensCombo->GetSelectedItem().IsValid() ? FText::FromString(UTF8_TO_TCHAR( this->_pTokensCombo->GetSelectedItem()->name.c_str())) : FText::FromString(TEXT("")); })]; this->createRadioButton( pSession, pMainVerticalBox, this->_tokenSource, TokenSource::UseExisting, TEXT("Use an existing token"), true, SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(EVerticalAlignment::VAlign_Center) .AutoWidth() .MaxWidth(500.0f) .Padding(5.0f)[SNew(STextBlock) .Text(FText::FromString(TEXT("Token:")))] + SHorizontalBox::Slot() .VAlign(EVerticalAlignment::VAlign_Center) .Padding(5.0f) .AutoWidth()[this->_pTokensCombo.ToSharedRef()]); this->createRadioButton( pSession, pMainVerticalBox, this->_tokenSource, TokenSource::Specify, TEXT("Specify a token"), false, SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(EVerticalAlignment::VAlign_Center) .AutoWidth() .Padding(5.0f)[SNew(STextBlock) .Text(FText::FromString(TEXT("Token:")))] + SHorizontalBox::Slot() .VAlign(EVerticalAlignment::VAlign_Center) .Padding(5.0f) .AutoWidth() .MaxWidth(500.0f) [SNew(SEditableTextBox) .Text(this, &SelectCesiumIonToken::GetSpecifiedToken) .OnTextChanged( this, &SelectCesiumIonToken::SetSpecifiedToken) .MinDesiredWidth(500.0f)]); pMainVerticalBox->AddSlot().AutoHeight().Padding( 5.0f, 20.0f, 5.0f, 5.0f)[SNew(SButton) .ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton") .TextStyle(FCesiumEditorModule::GetStyle(), "CesiumButtonText") .Visibility_Lambda([this]() { return this->_tokenSource == TokenSource ::Create ? EVisibility::Collapsed : EVisibility::Visible; }) .OnClicked(this, &SelectCesiumIonToken::UseOrCreate, pSession) .Text(FText::FromString(TEXT("Use as Project Default Token")))]; pMainVerticalBox->AddSlot().AutoHeight().Padding( 5.0f, 20.0f, 5.0f, 5.0f)[SNew(SButton) .ButtonStyle(FCesiumEditorModule::GetStyle(), "CesiumButton") .TextStyle(FCesiumEditorModule::GetStyle(), "CesiumButtonText") .Visibility_Lambda([this]() { return this->_tokenSource == TokenSource ::Create ? EVisibility::Visible : EVisibility::Collapsed; }) .OnClicked(this, &SelectCesiumIonToken::UseOrCreate, pSession) .Text(FText::FromString( TEXT("Create New Project Default Token")))]; TSharedRef totalBox = SNew(SVerticalBox); totalBox->AddSlot().AutoHeight()[pLoaderOrContent]; totalBox->AddSlot().AutoHeight()[pSingleUserText]; SWindow::Construct( SWindow::FArguments() .Title(FText::FromString(TEXT("Select a Cesium ion Token"))) .AutoCenter(EAutoCenter::PreferredWorkArea) .SizingRule(ESizingRule::UserSized) .ClientSize(FVector2D( 635, 500))[SNew(SBorder) .Visibility(EVisibility::Visible) .Padding( FMargin(10.0f, 10.0f, 10.0f, 10.0f))[totalBox]]); pSession->refreshTokens(); } void SelectCesiumIonToken::createRadioButton( const std::shared_ptr& pSession, const TSharedRef& pVertical, TokenSource& tokenSource, TokenSource thisValue, const FString& label, bool requiresIonConnection, const TSharedRef& pWidget) { auto visibility = [pSession, requiresIonConnection]() { if (!requiresIonConnection) { return EVisibility::Visible; } else if (pSession->isConnected()) { return EVisibility::Visible; } else { return EVisibility::Collapsed; } }; pVertical->AddSlot().AutoHeight().Padding(5.0f, 10.0f, 5.0f, 10.0f) [SNew(SCheckBox) .Visibility_Lambda(visibility) .Padding(5.0f) .Style(FAppStyle::Get(), "RadioButton") .IsChecked_Lambda([&tokenSource, thisValue]() { return tokenSource == thisValue ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; }) .OnCheckStateChanged_Lambda([&tokenSource, thisValue](ECheckBoxState newState) { if (newState == ECheckBoxState::Checked) { tokenSource = thisValue; } })[SNew(SBorder) [SNew(SVerticalBox) + SVerticalBox::Slot().Padding(5.0f).AutoHeight() [SNew(STextBlock) .TextStyle( FCesiumEditorModule::GetStyle(), "BodyBold") .Text(FText::FromString(label))] + SVerticalBox::Slot().Padding(5.0f).AutoHeight()[pWidget]]]]; } FReply SelectCesiumIonToken::UseOrCreate(std::shared_ptr pSession) { if (!this->_promise || !this->_future) { return FReply::Handled(); } Promise> promise = std::move(*this->_promise); this->_promise.reset(); TSharedRef pPanel = StaticCastSharedRef(this->AsShared()); auto getToken = [pPanel, pSession]() { const AsyncSystem& asyncSystem = getAsyncSystem(); if (pPanel->_tokenSource == TokenSource::Create) { if (pPanel->_createNewToken.name.IsEmpty()) { return asyncSystem.createResolvedFuture(Response()); } // Create a new token, initially only with access to asset ID 1 (Cesium // World Terrain). return pSession->getConnection()->createToken( TCHAR_TO_UTF8(*pPanel->_createNewToken.name), {"assets:read"}, std::vector{1}, std::nullopt); } else if (pPanel->_tokenSource == TokenSource::UseExisting) { return asyncSystem.createResolvedFuture( Response(Token(pPanel->_useExistingToken.token), 200, "", "")); } else if (pPanel->_tokenSource == TokenSource::Specify) { // Check if this is a known token, and use it if so. return pSession->findToken(pPanel->_specifyToken.token) .thenInMainThread([pPanel](Response&& response) { if (response.value) { return std::move(response); } else { Token t; t.token = TCHAR_TO_UTF8(*pPanel->_specifyToken.token); return Response(std::move(t), 200, "", ""); } }); } else { return asyncSystem.createResolvedFuture( Response(0, "UNKNOWNSOURCE", "The token source is unknown.")); } }; getToken().thenInMainThread([pPanel, pSession, promise = std::move(promise)]( Response&& response) { if (response.value) { pSession->invalidateProjectDefaultTokenDetails(); UCesiumIonServer* pServer = pPanel->_pServer.Get(); FScopedTransaction transaction( FText::FromString("Set Project Default Token")); pServer->DefaultIonAccessTokenId = UTF8_TO_TCHAR(response.value->id.c_str()); pServer->DefaultIonAccessToken = UTF8_TO_TCHAR(response.value->token.c_str()); pServer->Modify(); // Refresh all tilesets and overlays that are using the project // default token. UWorld* pWorld = GEditor->GetEditorWorldContext().World(); for (auto it = TActorIterator(pWorld); it; ++it) { if (it->GetTilesetSource() == ETilesetSource::FromCesiumIon && it->GetIonAccessToken().IsEmpty() && it->GetCesiumIonServer() == pServer) { it->RefreshTileset(); } else { // Tileset itself does not need to be refreshed, but maybe some // overlays do. TArray rasterOverlays; it->GetComponents(rasterOverlays); for (UCesiumIonRasterOverlay* pOverlay : rasterOverlays) { if (pOverlay->IonAccessToken.IsEmpty() && pOverlay->CesiumIonServer == pServer) { pOverlay->Refresh(); } } } } } else { UE_LOG( LogCesiumEditor, Error, TEXT("An error occurred while selecting a token: %s"), UTF8_TO_TCHAR(response.errorMessage.c_str())); } promise.resolve(std::move(response.value)); pPanel->RequestDestroyWindow(); }); while (!this->_future->isReady()) { getAssetAccessor()->tick(); getAsyncSystem().dispatchMainThreadTasks(); } return FReply::Handled(); } void SelectCesiumIonToken::RefreshTokens() { const std::vector& tokens = FCesiumEditorModule::serverManager() .GetSession(this->_pServer.Get()) ->getTokens(); this->_tokens.SetNum(tokens.size()); std::string createName = TCHAR_TO_UTF8(*this->_createNewToken.name); std::string specifiedToken = TCHAR_TO_UTF8(*this->_specifyToken.token); for (size_t i = 0; i < tokens.size(); ++i) { if (this->_tokens[i]) { *this->_tokens[i] = std::move(tokens[i]); } else { this->_tokens[i] = MakeShared(std::move(tokens[i])); } if (this->_tokens[i]->id == this->_useExistingToken.token.id) { this->_pTokensCombo->SetSelectedItem(this->_tokens[i]); this->_tokenSource = TokenSource::UseExisting; } // If there's already a token with the default name we would use to create a // new one, default to selecting that rather than creating a new one. if (this->_tokenSource == TokenSource::Create && this->_tokens[i]->name == createName) { this->_pTokensCombo->SetSelectedItem(this->_tokens[i]); this->_tokenSource = TokenSource::UseExisting; } // If this happens to be the specified token, select it. if (this->_tokenSource == TokenSource::Specify && this->_tokens[i]->token == specifiedToken) { this->_pTokensCombo->SetSelectedItem(this->_tokens[i]); this->_tokenSource = TokenSource::UseExisting; } } this->_pTokensCombo->RefreshOptions(); } TSharedRef SelectCesiumIonToken::OnGenerateTokenComboBoxEntry( TSharedPtr pToken) { return SNew(STextBlock) .Text(FText::FromString(UTF8_TO_TCHAR(pToken->name.c_str()))); } FText SelectCesiumIonToken::GetNewTokenName() const { return FText::FromString(this->_createNewToken.name); } void SelectCesiumIonToken::SetNewTokenName(const FText& text) { this->_createNewToken.name = text.ToString(); } void SelectCesiumIonToken::OnSelectExistingToken( TSharedPtr pToken, ESelectInfo::Type type) { if (pToken) { this->_useExistingToken.token = *pToken; } } FText SelectCesiumIonToken::GetSpecifiedToken() const { return FText::FromString(this->_specifyToken.token); } void SelectCesiumIonToken::SetSpecifiedToken(const FText& text) { this->_specifyToken.token = text.ToString(); }