311 lines
8.2 KiB
C++
311 lines
8.2 KiB
C++
#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
|