Tooling
This commit is contained in:
@@ -2,12 +2,15 @@
|
||||
#include "SaltyEditor.h"
|
||||
#include "Landscape.h"
|
||||
#include "LandscapeInfo.h"
|
||||
#include "LandscapeComponent.h"
|
||||
#include "LandscapeImportHelper.h"
|
||||
#include "LandscapeLayerInfoObject.h"
|
||||
#include "LandscapeSubsystem.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "RenderingThread.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
ALandscape* ULandscapeAuthoringLibrary::CreateLandscapeFromHeightmap(UWorld* World, const FString& HeightmapFile,
|
||||
const TArray<FLandscapeAuthoringWeightmap>& Weightmaps, UMaterialInterface* Material,
|
||||
@@ -80,6 +83,22 @@ ALandscape* ULandscapeAuthoringLibrary::CreateLandscapeFromHeightmap(UWorld* Wor
|
||||
FLandscapeImportHelper::TransformWeightmapImportData(Info.LayerData, Expanded, WeightDescriptor.ImportResolutions[0], RequiredResolution, ELandscapeImportTransformType::ExpandCentered);
|
||||
Info.LayerData = MoveTemp(Expanded);
|
||||
}
|
||||
|
||||
// An empty layer would be dropped without a word further down: ALandscapeProxy::Import only calls
|
||||
// SetAlphaData when `MaterialLayerInfo.LayerData.Num() > 0`, so no samples here would mean a layer that
|
||||
// is silently never painted. It should not be reachable - FLandscapeImportHelper's reader calls
|
||||
// SetNumZeroed(TotalPixels) before it decodes anything, so a descriptor that resolved at all comes back
|
||||
// full-sized - which is exactly why it is worth a check rather than a comment: the day it is reachable
|
||||
// is the day a whole world comes out shaded as the material's first layer with nothing logged anywhere.
|
||||
if (Info.LayerData.Num() == 0)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: layer %s read no samples from %s; the engine would have dropped it silently"),
|
||||
*Info.LayerName.ToString(), *Weightmap.File);
|
||||
return nullptr;
|
||||
}
|
||||
UE_LOG(LogSaltyEditor, Log, TEXT("CreateLandscapeFromHeightmap: layer %s <- %s, %d samples at %ux%u"),
|
||||
*Info.LayerName.ToString(), *Weightmap.File, Info.LayerData.Num(),
|
||||
WeightDescriptor.ImportResolutions[0].Width, WeightDescriptor.ImportResolutions[0].Height);
|
||||
LayerInfos.Add(MoveTemp(Info));
|
||||
}
|
||||
|
||||
@@ -146,7 +165,80 @@ ALandscape* ULandscapeAuthoringLibrary::CreateLandscapeFromHeightmap(UWorld* Wor
|
||||
UE_LOG(LogSaltyEditor, Warning, TEXT("CreateLandscapeFromHeightmap: the edit-layer update did not finish; render heightmaps may be stale"));
|
||||
}
|
||||
|
||||
// Did the weights actually survive the import and the merge? A component with no weightmap allocations is a
|
||||
// landscape the material will shade entirely from its preview weights, which looks like a material problem
|
||||
// and is not one. Checked rather than assumed, because every failure mode above this line is silent.
|
||||
{
|
||||
TArray<ULandscapeComponent*> Components;
|
||||
Landscape->GetComponents<ULandscapeComponent>(Components);
|
||||
if (Components.Num() > 0 && Components[0] != nullptr)
|
||||
{
|
||||
const int32 Allocations = Components[0]->GetWeightmapLayerAllocations().Num();
|
||||
const int32 Textures = Components[0]->GetWeightmapTextures().Num();
|
||||
if (Allocations == 0 && LayerInfos.Num() > 0)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLandscapeFromHeightmap: %d layers were imported but component 0 has no weightmap allocations; the ground will render as the material's first layer everywhere"),
|
||||
LayerInfos.Num());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Log, TEXT("CreateLandscapeFromHeightmap: component 0 carries %d weightmap allocation(s) across %d texture(s)"),
|
||||
Allocations, Textures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogSaltyEditor, Log, TEXT("CreateLandscapeFromHeightmap: %ux%u vertices, %dx%d components of %d quads, scale (%g, %g, %g), %d paint layers"),
|
||||
RequiredResolution.Width, RequiredResolution.Height, ComponentCount.X, ComponentCount.Y, QuadsPerComponent, Scale.X, Scale.Y, Scale.Z, LayerInfos.Num());
|
||||
return Landscape;
|
||||
}
|
||||
|
||||
ULandscapeLayerInfoObject* ULandscapeAuthoringLibrary::CreateLayerInfo(const FString& PackagePath,
|
||||
const FString& AssetName, FName LayerName)
|
||||
{
|
||||
if (PackagePath.IsEmpty() || AssetName.IsEmpty() || LayerName.IsNone())
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLayerInfo: needs a package path, an asset name and a layer name"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const FString PackageName = PackagePath / AssetName;
|
||||
|
||||
// Rerunnable: an existing layer info is returned as it is. Replacing one would break every landscape that
|
||||
// already paints with it, and the thing a caller wants here is "make sure this exists".
|
||||
if (ULandscapeLayerInfoObject* Existing = LoadObject<ULandscapeLayerInfoObject>(nullptr, *(PackageName + TEXT(".") + AssetName)))
|
||||
{
|
||||
if (Existing->GetLayerName() != LayerName)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Warning, TEXT("CreateLayerInfo: %s already exists and its LayerName is %s, not %s; left alone"),
|
||||
*PackageName, *Existing->GetLayerName().ToString(), *LayerName.ToString());
|
||||
}
|
||||
return Existing;
|
||||
}
|
||||
|
||||
UPackage* Package = CreatePackage(*PackageName);
|
||||
if (!Package)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLayerInfo: could not create package %s"), *PackageName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ULandscapeLayerInfoObject* Info = NewObject<ULandscapeLayerInfoObject>(
|
||||
Package, *AssetName, RF_Public | RF_Standalone | RF_Transactional);
|
||||
if (!Info)
|
||||
{
|
||||
UE_LOG(LogSaltyEditor, Error, TEXT("CreateLayerInfo: could not create %s"), *PackageName);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The whole reason this function exists: LayerName is VisibleAnywhere, so Python cannot set it and a
|
||||
// duplicated layer info keeps the name it was copied from. Through the setter, not the member: the member
|
||||
// is deprecated and goes private next release, and bInModify false because the object was made a moment
|
||||
// ago and has nothing to record an undo against.
|
||||
Info->SetLayerName(LayerName, /*bInModify=*/false);
|
||||
Info->MarkPackageDirty();
|
||||
FAssetRegistryModule::AssetCreated(Info);
|
||||
|
||||
UE_LOG(LogSaltyEditor, Log, TEXT("CreateLayerInfo: %s with LayerName %s"), *PackageName, *LayerName.ToString());
|
||||
return Info;
|
||||
}
|
||||
|
||||
@@ -40,4 +40,15 @@ public:
|
||||
static ALandscape* CreateLandscapeFromHeightmap(UWorld* World, const FString& HeightmapFile,
|
||||
const TArray<FLandscapeAuthoringWeightmap>& Weightmaps, UMaterialInterface* Material,
|
||||
FVector Location, FVector Scale, int32 WorldPartitionGridSize = 4);
|
||||
|
||||
// Creates a ULandscapeLayerInfoObject asset carrying LayerName, which is the name the landscape material
|
||||
// blends by and the name a weightmap is imported against.
|
||||
//
|
||||
// The second thing the engine exposes no Python path to, and for the same reason as the first. There is no
|
||||
// LayerInfo factory in the bindings, and ULandscapeLayerInfoObject::LayerName is VisibleAnywhere - so the
|
||||
// obvious workaround, duplicating an existing layer info, produces an asset that silently keeps the name it
|
||||
// was copied from and paints that substance wherever the new layer should be. Returns null and logs why on
|
||||
// failure; an existing asset at the path is returned rather than replaced, so this is safe to rerun.
|
||||
UFUNCTION(BlueprintCallable, Category = "Salty|Authoring")
|
||||
static ULandscapeLayerInfoObject* CreateLayerInfo(const FString& PackagePath, const FString& AssetName, FName LayerName);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ using UnrealBuildTool;
|
||||
|
||||
// Editor-only tools. Arrived with the first one (Docs/Spec/Architecture.md, Modules): the landscape authoring
|
||||
// library that Scripts/Authoring/create_world.py calls, because the engine exposes no landscape creation to Python.
|
||||
// The World Map tab joined it: the same SWorldMap the game draws, in a dockable tab.
|
||||
public class SaltyEditor : ModuleRules
|
||||
{
|
||||
public SaltyEditor(ReadOnlyTargetRules Target) : base(Target)
|
||||
@@ -18,7 +19,17 @@ public class SaltyEditor : ModuleRules
|
||||
"UnrealEd",
|
||||
"Landscape",
|
||||
"LandscapeEditor", // FLandscapeImportHelper reads the heightmap and weightmap files
|
||||
"RenderCore" // FlushRenderingCommands while the edit-layer merge is driven by hand
|
||||
"RenderCore", // FlushRenderingCommands while the edit-layer merge is driven by hand
|
||||
|
||||
// The World Map tab. It depends on the game module because it hosts the game's own map widget
|
||||
// rather than a second copy of it; an editor module may depend on a runtime one, never the reverse.
|
||||
"Salty",
|
||||
"SaltyCore",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"InputCore",
|
||||
"AssetRegistry", // finding every UWorldMapDefinition in the project
|
||||
"WorkspaceMenuStructure" // putting the tab in the Window menu
|
||||
});
|
||||
|
||||
PublicIncludePaths.Add(ModuleDirectory);
|
||||
|
||||
@@ -1,6 +1,39 @@
|
||||
#include "SaltyEditor.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, SaltyEditor);
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "WorldMap/WorldMapTab.h"
|
||||
|
||||
// The editor module now has a startup of its own: the World Map tab registers here. Before it there was
|
||||
// nothing to start and FDefaultModuleImpl was enough.
|
||||
class FSaltyEditorModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
virtual void StartupModule() override
|
||||
{
|
||||
// A commandlet - Scripts/Authoring runs several through -run=pythonscript - loads editor modules with
|
||||
// no Slate at all, and registering a tab spawner there would crash on the way to building a level.
|
||||
if (IsRunningCommandlet() || !FSlateApplication::IsInitialized())
|
||||
{
|
||||
return;
|
||||
}
|
||||
SaltyWorldMapTab::Register();
|
||||
bRegisteredTab = true;
|
||||
}
|
||||
|
||||
virtual void ShutdownModule() override
|
||||
{
|
||||
if (bRegisteredTab && FSlateApplication::IsInitialized())
|
||||
{
|
||||
SaltyWorldMapTab::Unregister();
|
||||
}
|
||||
bRegisteredTab = false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool bRegisteredTab = false;
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FSaltyEditorModule, SaltyEditor);
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogSaltyEditor);
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
#include "WorldMap/WorldMapTab.h"
|
||||
|
||||
#include "AssetRegistry/ARFilter.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Editor.h"
|
||||
#include "Framework/Docking/TabManager.h"
|
||||
#include "LevelEditorViewport.h"
|
||||
#include "SaltyEditor.h"
|
||||
#include "UI/SWorldMap.h"
|
||||
#include "Widgets/Docking/SDockTab.h"
|
||||
#include "Widgets/Input/SButton.h"
|
||||
#include "Widgets/Input/SCheckBox.h"
|
||||
#include "Widgets/Input/SComboBox.h"
|
||||
#include "Styling/AppStyle.h"
|
||||
#include "Widgets/SBoxPanel.h"
|
||||
#include "Widgets/Layout/SBorder.h"
|
||||
#include "Widgets/Layout/SSeparator.h"
|
||||
#include "Widgets/Text/STextBlock.h"
|
||||
#include "WorkspaceMenuStructure.h"
|
||||
#include "WorkspaceMenuStructureModule.h"
|
||||
#include "World/WorldMapDefinition.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "SaltyWorldMapTab"
|
||||
|
||||
namespace SaltyWorldMapTab
|
||||
{
|
||||
const FName TabId("SaltyWorldMap");
|
||||
}
|
||||
|
||||
void SWorldMapPanel::Construct(const FArguments& InArgs)
|
||||
{
|
||||
FindDefinitions();
|
||||
|
||||
ChildSlot
|
||||
[
|
||||
SNew(SVerticalBox)
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.AutoHeight()
|
||||
[
|
||||
SNew(SBorder)
|
||||
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
|
||||
.Padding(4.f)
|
||||
[
|
||||
MakeToolbar()
|
||||
]
|
||||
]
|
||||
|
||||
+ SVerticalBox::Slot()
|
||||
.FillHeight(1.f)
|
||||
[
|
||||
SAssignNew(Map, SWorldMap)
|
||||
.Definition(Selected.Get())
|
||||
.ShowGrid(true)
|
||||
.ShowScaleBar(true)
|
||||
// No subsystem in an editor world, so neither of these has anything to draw. Said explicitly
|
||||
// rather than left to default, because "where is my player arrow" is otherwise a puzzle.
|
||||
.ShowMarkers(false)
|
||||
.ShowLocalPlayer(false)
|
||||
.OnClicked(FOnWorldMapClicked::CreateSP(this, &SWorldMapPanel::HandleMapClicked))
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
void SWorldMapPanel::FindDefinitions()
|
||||
{
|
||||
Definitions.Reset();
|
||||
|
||||
const FAssetRegistryModule& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
||||
FARFilter Filter;
|
||||
Filter.ClassPaths.Add(UWorldMapDefinition::StaticClass()->GetClassPathName());
|
||||
Filter.bRecursiveClasses = true;
|
||||
|
||||
TArray<FAssetData> Found;
|
||||
Registry.Get().GetAssets(Filter, Found);
|
||||
|
||||
for (const FAssetData& Asset : Found)
|
||||
{
|
||||
if (UWorldMapDefinition* Definition = Cast<UWorldMapDefinition>(Asset.GetAsset()))
|
||||
{
|
||||
Definitions.Add(Definition);
|
||||
}
|
||||
}
|
||||
// A stable order, so the combo does not shuffle between sessions.
|
||||
Definitions.Sort([](const TWeakObjectPtr<UWorldMapDefinition>& A, const TWeakObjectPtr<UWorldMapDefinition>& B)
|
||||
{
|
||||
return A.IsValid() && B.IsValid() && A->GetName() < B->GetName();
|
||||
});
|
||||
|
||||
if (!Selected.IsValid() && Definitions.Num() > 0)
|
||||
{
|
||||
Selected = Definitions[0];
|
||||
}
|
||||
|
||||
LayerOptions.Reset();
|
||||
if (const UWorldMapDefinition* Definition = Selected.Get())
|
||||
{
|
||||
for (const FName& Id : Definition->GetLayerIds())
|
||||
{
|
||||
LayerOptions.Add(MakeShared<FName>(Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SWorldMapPanel::SelectDefinition(TWeakObjectPtr<UWorldMapDefinition> Choice)
|
||||
{
|
||||
Selected = Choice;
|
||||
LayerOptions.Reset();
|
||||
if (const UWorldMapDefinition* Definition = Selected.Get())
|
||||
{
|
||||
for (const FName& Id : Definition->GetLayerIds())
|
||||
{
|
||||
LayerOptions.Add(MakeShared<FName>(Id));
|
||||
}
|
||||
}
|
||||
if (Map.IsValid())
|
||||
{
|
||||
Map->SetDefinition(Selected.Get());
|
||||
Map->ZoomToFit();
|
||||
}
|
||||
if (LayerCombo.IsValid())
|
||||
{
|
||||
LayerCombo->RefreshOptions();
|
||||
}
|
||||
}
|
||||
|
||||
void SWorldMapPanel::HandleMapClicked(FVector2D WorldCm)
|
||||
{
|
||||
// Move every perspective viewport, because "the active one" is whichever was clicked last and that was
|
||||
// this tab. Keep the altitude: a click on a map is "look over there", not "drop to sea level".
|
||||
int32 Moved = 0;
|
||||
for (FLevelEditorViewportClient* Client : GEditor->GetLevelViewportClients())
|
||||
{
|
||||
if (!Client || !Client->IsPerspective())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FVector Location = Client->GetViewLocation();
|
||||
Location.X = WorldCm.X;
|
||||
Location.Y = WorldCm.Y;
|
||||
Client->SetViewLocation(Location);
|
||||
Client->Invalidate();
|
||||
++Moved;
|
||||
}
|
||||
|
||||
const FVector2D WorldM = WorldCm / 100.0;
|
||||
Status = Moved > 0
|
||||
? FText::Format(LOCTEXT("Moved", "Viewport moved to X {0} m, Y {1} m"),
|
||||
FText::AsNumber(FMath::RoundToInt(WorldM.X)), FText::AsNumber(FMath::RoundToInt(WorldM.Y)))
|
||||
: LOCTEXT("NoViewport", "No perspective viewport to move.");
|
||||
|
||||
UE_LOG(LogSaltyEditor, Log, TEXT("World map: clicked X %.0f m, Y %.0f m; moved %d viewport(s)"),
|
||||
WorldM.X, WorldM.Y, Moved);
|
||||
}
|
||||
|
||||
FText SWorldMapPanel::DefinitionLabel() const
|
||||
{
|
||||
const UWorldMapDefinition* Definition = Selected.Get();
|
||||
return Definition ? FText::FromString(Definition->GetName()) : LOCTEXT("NoDefinition", "No map found");
|
||||
}
|
||||
|
||||
FText SWorldMapPanel::LayerLabel() const
|
||||
{
|
||||
return Map.IsValid() ? FText::FromName(Map->GetLayer()) : FText::GetEmpty();
|
||||
}
|
||||
|
||||
FText SWorldMapPanel::StatusLabel() const
|
||||
{
|
||||
if (!Status.IsEmpty())
|
||||
{
|
||||
return Status;
|
||||
}
|
||||
const UWorldMapDefinition* Definition = Selected.Get();
|
||||
return Definition
|
||||
? FText::FromString(Definition->Projection.ToString())
|
||||
: LOCTEXT("BuildHint", "Run Scripts/Authoring/build_world_map.sh to build one.");
|
||||
}
|
||||
|
||||
TSharedRef<SWidget> SWorldMapPanel::MakeToolbar()
|
||||
{
|
||||
return SNew(SHorizontalBox)
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 6, 0).VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SComboBox<TWeakObjectPtr<UWorldMapDefinition>>)
|
||||
.OptionsSource(&Definitions)
|
||||
.OnGenerateWidget_Lambda([](TWeakObjectPtr<UWorldMapDefinition> Item)
|
||||
{
|
||||
return SNew(STextBlock).Text(FText::FromString(Item.IsValid() ? Item->GetName() : TEXT("(missing)")));
|
||||
})
|
||||
.OnSelectionChanged_Lambda([this](TWeakObjectPtr<UWorldMapDefinition> Item, ESelectInfo::Type)
|
||||
{
|
||||
if (Item.IsValid())
|
||||
{
|
||||
SelectDefinition(Item);
|
||||
}
|
||||
})
|
||||
[
|
||||
SNew(STextBlock).Text(this, &SWorldMapPanel::DefinitionLabel)
|
||||
]
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 6, 0).VAlign(VAlign_Center)
|
||||
[
|
||||
SAssignNew(LayerCombo, SComboBox<TSharedPtr<FName>>)
|
||||
.OptionsSource(&LayerOptions)
|
||||
.OnGenerateWidget_Lambda([](TSharedPtr<FName> Item)
|
||||
{
|
||||
return SNew(STextBlock).Text(FText::FromName(Item.IsValid() ? *Item : NAME_None));
|
||||
})
|
||||
.OnSelectionChanged_Lambda([this](TSharedPtr<FName> Item, ESelectInfo::Type)
|
||||
{
|
||||
if (Item.IsValid() && Map.IsValid())
|
||||
{
|
||||
Map->SetLayer(*Item);
|
||||
}
|
||||
})
|
||||
[
|
||||
SNew(STextBlock).Text(this, &SWorldMapPanel::LayerLabel)
|
||||
]
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 6, 0).VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SCheckBox)
|
||||
.IsChecked(ECheckBoxState::Checked)
|
||||
.OnCheckStateChanged_Lambda([this](ECheckBoxState State)
|
||||
{
|
||||
if (Map.IsValid())
|
||||
{
|
||||
Map->SetShowGrid(State == ECheckBoxState::Checked);
|
||||
}
|
||||
})
|
||||
[
|
||||
SNew(STextBlock).Text(LOCTEXT("Grid", "Grid"))
|
||||
]
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 6, 0).VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Fit", "Fit"))
|
||||
.ToolTipText(LOCTEXT("FitTip", "Zoom out until the whole world fits. Right-double-click the map does the same."))
|
||||
.OnClicked_Lambda([this]()
|
||||
{
|
||||
if (Map.IsValid())
|
||||
{
|
||||
Map->ZoomToFit();
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 6, 0).VAlign(VAlign_Center)
|
||||
[
|
||||
SNew(SButton)
|
||||
.Text(LOCTEXT("Refresh", "Refresh"))
|
||||
.ToolTipText(LOCTEXT("RefreshTip", "Look for map definitions again, after a rebuild."))
|
||||
.OnClicked_Lambda([this]()
|
||||
{
|
||||
FindDefinitions();
|
||||
if (Map.IsValid())
|
||||
{
|
||||
Map->SetDefinition(Selected.Get());
|
||||
}
|
||||
if (LayerCombo.IsValid())
|
||||
{
|
||||
LayerCombo->RefreshOptions();
|
||||
}
|
||||
return FReply::Handled();
|
||||
})
|
||||
]
|
||||
|
||||
+ SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center).Padding(6, 0, 0, 0)
|
||||
[
|
||||
SNew(STextBlock)
|
||||
.Text(this, &SWorldMapPanel::StatusLabel)
|
||||
.ColorAndOpacity(FSlateColor::UseSubduedForeground())
|
||||
];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
namespace SaltyWorldMapTab
|
||||
{
|
||||
static TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs&)
|
||||
{
|
||||
return SNew(SDockTab)
|
||||
.TabRole(ETabRole::NomadTab)
|
||||
[
|
||||
SNew(SWorldMapPanel)
|
||||
];
|
||||
}
|
||||
|
||||
void Register()
|
||||
{
|
||||
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(TabId, FOnSpawnTab::CreateStatic(&SpawnTab))
|
||||
.SetDisplayName(LOCTEXT("TabTitle", "World Map"))
|
||||
.SetTooltipText(LOCTEXT("TabTooltip", "The world's map. Click it to move the viewport camera."))
|
||||
.SetGroup(WorkspaceMenu::GetMenuStructure().GetLevelEditorCategory())
|
||||
.SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.WorldBrowser"));
|
||||
}
|
||||
|
||||
void Unregister()
|
||||
{
|
||||
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(TabId);
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Widgets/SCompoundWidget.h"
|
||||
|
||||
class SWorldMap;
|
||||
class UWorldMapDefinition;
|
||||
template <typename T> class SComboBox;
|
||||
|
||||
/**
|
||||
* The World Map tab: the same SWorldMap the game draws, with a strip of controls above it and a click that
|
||||
* takes the viewport camera somewhere.
|
||||
*
|
||||
* The map widget is shared rather than reimplemented, which is the point of it being Slate. What differs here
|
||||
* is only what an editor has and a game does not: a choice of which world's map to look at, and a viewport to
|
||||
* send somewhere. There are no markers, because markers come from a UWorldMapSubsystem and an editor world
|
||||
* has none - that is not a gap, it is that nobody is playing.
|
||||
*/
|
||||
class SWorldMapPanel : public SCompoundWidget
|
||||
{
|
||||
public:
|
||||
SLATE_BEGIN_ARGS(SWorldMapPanel) {}
|
||||
SLATE_END_ARGS()
|
||||
|
||||
void Construct(const FArguments& InArgs);
|
||||
|
||||
private:
|
||||
/** Every UWorldMapDefinition in the project, by asset registry. Editor-only, which is why it may do this. */
|
||||
void FindDefinitions();
|
||||
|
||||
void SelectDefinition(TWeakObjectPtr<UWorldMapDefinition> Choice);
|
||||
void HandleMapClicked(FVector2D WorldCm);
|
||||
|
||||
TSharedRef<SWidget> MakeToolbar();
|
||||
FText DefinitionLabel() const;
|
||||
FText LayerLabel() const;
|
||||
FText StatusLabel() const;
|
||||
|
||||
TArray<TWeakObjectPtr<UWorldMapDefinition>> Definitions;
|
||||
TWeakObjectPtr<UWorldMapDefinition> Selected;
|
||||
|
||||
TArray<TSharedPtr<FName>> LayerOptions;
|
||||
|
||||
TSharedPtr<SWorldMap> Map;
|
||||
TSharedPtr<SComboBox<TSharedPtr<FName>>> LayerCombo;
|
||||
|
||||
/** What the last click did, so a click that could not move a camera says why rather than doing nothing. */
|
||||
FText Status;
|
||||
};
|
||||
|
||||
/** Registers and unregisters the tab with the global tab manager. Called by the module. */
|
||||
namespace SaltyWorldMapTab
|
||||
{
|
||||
extern const FName TabId;
|
||||
|
||||
void Register();
|
||||
void Unregister();
|
||||
}
|
||||
Reference in New Issue
Block a user