Files
BXSSP_Andriod/Plugins/CesiumForUnreal/Source/CesiumEditor/Private/SelectCesiumIonToken.cpp

622 lines
23 KiB
C++

// 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>
SelectCesiumIonToken::_pExistingPanel{};
/*static*/ SharedFuture<std::optional<Token>>
SelectCesiumIonToken::SelectNewToken(UCesiumIonServer* pServer) {
std::shared_ptr<CesiumIonSession> 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>(Token()))
.share();
}
if (SelectCesiumIonToken::_pExistingPanel.IsValid()) {
SelectCesiumIonToken::_pExistingPanel->BringToFront();
} else {
TSharedRef<SelectCesiumIonToken> Panel =
SNew(SelectCesiumIonToken).Server(pServer);
SelectCesiumIonToken::_pExistingPanel = Panel;
Panel->_promise = getAsyncSystem().createPromise<std::optional<Token>>();
Panel->_future = Panel->_promise->getFuture().share();
Panel->GetOnWindowClosedEvent().AddLambda(
[Panel](const TSharedRef<SWindow>& 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<std::optional<Token>>
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<Token>& maybeToken) {
return maybeToken;
});
} else {
return getAsyncSystem().createResolvedFuture(
std::make_optional(token));
}
});
}
namespace {
std::vector<int64_t> findUnauthorizedAssets(
const std::vector<int64_t>& authorizedAssets,
const std::vector<int64_t>& requiredAssets) {
std::vector<int64_t> 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<std::optional<Token>> SelectCesiumIonToken::SelectAndAuthorizeToken(
UCesiumIonServer* pServer,
const std::vector<int64_t>& assetIDs) {
std::shared_ptr<CesiumIonSession> 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>(Token()));
}
return SelectTokenIfNecessary(pServer).thenInMainThread([pSession, assetIDs](
const std::optional<
Token>&
maybeToken) {
const std::optional<Connection>& maybeConnection =
pSession->getConnection();
if (maybeConnection && maybeToken && !maybeToken->id.empty() &&
maybeToken->assetIds) {
std::vector<int64_t> 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<Token>&& response) {
if (response.value) {
std::vector<int64_t> missingAssets =
findUnauthorizedAssets(*maybeToken->assetIds, assetIDs);
if (!missingAssets.empty()) {
std::vector<std::string> 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<NoValue>&&) {
return maybeToken;
});
}
}
return getAsyncSystem().createResolvedFuture(
std::optional<Token>(maybeToken));
});
}
}
return getAsyncSystem().createResolvedFuture(
std::optional<Token>(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<CesiumIonSession> pSession =
FCesiumEditorModule::serverManager().GetSession(this->_pServer.Get());
this->_tokensUpdatedDelegateHandle = pSession->TokensUpdated.AddRaw(
this,
&SelectCesiumIonToken::RefreshTokens);
TSharedRef<SVerticalBox> pLoaderOrContent =
SNew(SVerticalBox).Visibility_Lambda([pSession]() {
return pSession->getAppData().needsOauthAuthentication()
? EVisibility::Visible
: EVisibility::Collapsed;
});
TSharedRef<SVerticalBox> 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<SVerticalBox> 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<TSharedPtr<Token>>)
.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<SVerticalBox> 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<CesiumIonSession>& pSession,
const TSharedRef<SVerticalBox>& pVertical,
TokenSource& tokenSource,
TokenSource thisValue,
const FString& label,
bool requiresIonConnection,
const TSharedRef<SWidget>& 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<CesiumIonSession> pSession) {
if (!this->_promise || !this->_future) {
return FReply::Handled();
}
Promise<std::optional<Token>> promise = std::move(*this->_promise);
this->_promise.reset();
TSharedRef<SelectCesiumIonToken> pPanel =
StaticCastSharedRef<SelectCesiumIonToken>(this->AsShared());
auto getToken = [pPanel, pSession]() {
const AsyncSystem& asyncSystem = getAsyncSystem();
if (pPanel->_tokenSource == TokenSource::Create) {
if (pPanel->_createNewToken.name.IsEmpty()) {
return asyncSystem.createResolvedFuture(Response<Token>());
}
// 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<int64_t>{1},
std::nullopt);
} else if (pPanel->_tokenSource == TokenSource::UseExisting) {
return asyncSystem.createResolvedFuture(
Response<Token>(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<Token>&& 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<Token>(0, "UNKNOWNSOURCE", "The token source is unknown."));
}
};
getToken().thenInMainThread([pPanel, pSession, promise = std::move(promise)](
Response<Token>&& 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<ACesium3DTileset>(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<UCesiumIonRasterOverlay*> rasterOverlays;
it->GetComponents<UCesiumIonRasterOverlay>(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<Token>& 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<Token>(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<SWidget> SelectCesiumIonToken::OnGenerateTokenComboBoxEntry(
TSharedPtr<CesiumIonClient::Token> 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<CesiumIonClient::Token> 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();
}