Loading...
Loading...
Use when extending the Unreal Editor with editor tool, editor utility widget, Blutility, detail customization, property customization, editor mode, asset editor, editor subsystem, editor extension, UToolMenus, or scripted asset operations. For Slate fundamentals, see ue-ui-umg-slate. For module build config, see ue-module-build-system.
npx skill4agent add quodsoler/unreal-engine-skills ue-editor-tools.agents/ue-project-context.md"Type": "Editor"UnrealEdPropertyEditor#if WITH_EDITOR{ "Name": "MyGameEditor", "Type": "Editor", "LoadingPhase": "PostEngineInit" }// MyGameEditor.Build.cs — key dependencies
PrivateDependencyModuleNames.AddRange(new string[] {
"Core", "CoreUObject", "Engine", "UnrealEd",
"Slate", "SlateCore", "EditorStyle",
"PropertyEditor", // IDetailCustomization, IPropertyTypeCustomization
"EditorSubsystem", // UEditorSubsystem
"Blutility", // UEditorUtilityWidget, UAssetActionUtility
"ToolMenus", // UToolMenus
"AssetTools", // FAssetTypeActions_Base
"MyGame"
});StartupModuleShutdownModuleIMPLEMENT_MODULE(FMyGameEditorModule, MyGameEditor)
void FMyGameEditorModule::StartupModule()
{
FPropertyEditorModule& PropMod =
FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropMod.RegisterCustomClassLayout(
UMyDataAsset::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(
&FMyDataAssetCustomization::MakeInstance));
PropMod.NotifyCustomizationModuleChanged();
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(
this, &FMyGameEditorModule::RegisterMenus));
}
void FMyGameEditorModule::ShutdownModule()
{
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor")
.UnregisterCustomClassLayout(UMyDataAsset::StaticClass()->GetFName());
}
UToolMenus::UnRegisterStartupCallback(this);
if (UToolMenus* TM = UToolMenus::TryGet()) { TM->UnregisterOwner(this); }
}Full boilerplate with asset type actions, editor modes, and factory:references/editor-module-setup.md
UEditorUtilityWidgetEditorUtilityWidget.hUUserWidget#pragma once
#include "EditorUtilityWidget.h"
#include "MyEditorUtilityWidget.generated.h"
UCLASS()
class MYGAMEEDITOR_API UMyEditorUtilityWidget : public UEditorUtilityWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "My Tools")
void BatchRenameSelectedAssets(const FString& Prefix);
};
// .cpp
void UMyEditorUtilityWidget::BatchRenameSelectedAssets(const FString& Prefix)
{
for (UObject* Asset : UEditorUtilityLibrary::GetSelectedAssets())
{
if (Asset)
{
const FString Src = Asset->GetOutermost()->GetName(); // e.g. /Game/Folder/OldName
UEditorAssetLibrary::RenameAsset(Src, FPaths::GetPath(Src) / Prefix + TEXT("_") + Asset->GetName());
}
}
}UEditorUtilityLibraryEditorUtilityLibrary.h| Function | Purpose |
|---|---|
| Currently selected Content Browser assets |
| Filter selection by class |
| Delete asset; |
| Selected level actors |
| Sync content browser view |
FARFilterIAssetRegistry::GetAssetsGEditor->GetEditorSubsystem<UEditorUtilitySubsystem>()
->SpawnAndRegisterTab(LoadObject<UEditorUtilityWidgetBlueprint>(
nullptr, TEXT("/Game/EditorWidgets/BP_MyTool")));UFUNCTION(CallInEditor)UAssetActionUtilitySupportedClasses#pragma once
#include "AssetActionUtility.h" // Engine/Source/Editor/Blutility/Classes/AssetActionUtility.h
#include "MyAssetActionUtility.generated.h"
UCLASS()
class MYGAMEEDITOR_API UMyAssetActionUtility : public UAssetActionUtility
{
GENERATED_BODY()
public:
UFUNCTION(CallInEditor, Category = "My Tools")
void SetTextureCompressionToUI()
{
for (UObject* Asset : UEditorUtilityLibrary::GetSelectedAssets())
{
if (UTexture2D* Tex = Cast<UTexture2D>(Asset))
{
Tex->CompressionSettings = TC_EditorIcon;
Tex->MarkPackageDirty();
Tex->PostEditChange();
}
}
}
};UAssetActionUtilityUActorActionUtilityUEditorUtilityObjectclass FMyDataAssetCustomization : public IDetailCustomization
{
public:
static TSharedRef<IDetailCustomization> MakeInstance();
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
// Override the TSharedPtr overload too — store WeakBuilder for async ForceRefreshDetails
virtual void CustomizeDetails(const TSharedPtr<IDetailLayoutBuilder>& DetailBuilder) override;
private:
TWeakPtr<IDetailLayoutBuilder> WeakBuilder;
};
// In CustomizeDetails: EditCategory, AddProperty, AddCustomRow, HideCategory
// Register: PropMod.RegisterCustomClassLayout(UMyClass::StaticClass()->GetFName(), ...MakeInstance)
// Unregister in ShutdownModule: PropMod.UnregisterCustomClassLayout(...)class FMyStructCustomization : public IPropertyTypeCustomization
{
public:
static TSharedRef<IPropertyTypeCustomization> MakeInstance();
virtual void CustomizeHeader(TSharedRef<IPropertyHandle> Handle,
FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& Utils) override;
virtual void CustomizeChildren(TSharedRef<IPropertyHandle> Handle,
IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& Utils) override;
};
// Register: PropMod.RegisterCustomPropertyTypeLayout(FMyStruct::StaticStruct()->GetFName(), ...MakeInstance)Full implementations, Slate row patterns, IPropertyHandle read/write, NameContent/ValueContent:references/detail-customization-patterns.md
FEdModeEdMode.h// MyEditorMode.h
class FMyEditorMode : public FEdMode
{
public:
static const FEditorModeID EM_MyMode;
FMyEditorMode();
virtual void Enter() override;
virtual void Exit() override;
virtual bool HandleClick(FEditorViewportClient*, HHitProxy*,
const FViewportClick&) override;
virtual bool MouseMove(FEditorViewportClient* ViewportClient,
FViewport* Viewport, int32 X, int32 Y) override;
// Use View->DeprojectFVector2D (FSceneView) for world-space ray from pixel coords
virtual void Render(const FSceneView*, FViewport*,
FPrimitiveDrawInterface*) override;
virtual bool UsesToolkits() const override { return true; }
};
// MyEditorMode.cpp
const FEditorModeID FMyEditorMode::EM_MyMode = TEXT("EM_MyEditorMode");
FMyEditorMode::FMyEditorMode()
{
Info = FEditorModeInfo(EM_MyMode,
FText::FromString("My Editor Mode"), FSlateIcon(), /*bVisible=*/true);
}
void FMyEditorMode::Enter()
{
FEdMode::Enter();
if (!Toolkit.IsValid())
{
Toolkit = MakeShareable(new FMyEditorModeToolkit);
Toolkit->Init(Owner->GetToolkitHost());
}
}
void FMyEditorMode::Exit()
{
if (Toolkit.IsValid())
FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef());
FEdMode::Exit();
}
void FMyEditorMode::Render(const FSceneView* View, FViewport* Viewport,
FPrimitiveDrawInterface* PDI)
{
FEdMode::Render(View, Viewport, PDI);
DrawWireSphere(PDI, FVector::ZeroVector, FLinearColor::Green, 50.f, 16, SDPG_World);
}// StartupModule
FEditorModeRegistry::Get().RegisterMode<FMyEditorMode>(
FMyEditorMode::EM_MyMode, FText::FromString("My Mode"), FSlateIcon(), true);
// ShutdownModule
FEditorModeRegistry::Get().UnregisterMode(FMyEditorMode::EM_MyMode);FAssetTypeActions_BaseAssetTypeActions_Base.hclass FMyAssetTypeActions : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override { return FText::FromString("My Asset"); }
virtual FColor GetTypeColor() const override { return FColor(200, 100, 50); }
virtual UClass* GetSupportedClass() const override { return UMyAsset::StaticClass(); }
virtual uint32 GetCategories() override { return EAssetTypeCategories::Misc; }
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects,
TSharedPtr<IToolkitHost> EditWithinLevelEditor) override
{
for (UObject* Obj : InObjects)
if (UMyAsset* Asset = Cast<UMyAsset>(Obj))
MakeShareable(new FMyAssetEditor)->InitMyEditor(
EToolkitMode::Standalone, EditWithinLevelEditor, Asset);
}
};StartupModuleShutdownModuleclass FMyAssetEditor : public FAssetEditorToolkit
{
public:
void InitMyEditor(EToolkitMode::Type Mode, TSharedPtr<IToolkitHost> Host, UMyAsset* Asset);
virtual FName GetToolkitFName() const override { return "MyAssetEditor"; }
virtual FText GetBaseToolkitName() const override { return INVTEXT("My Asset Editor"); }
virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& TabMgr) override;
virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& TabMgr) override;
};
// Call InitAssetEditor() inside InitMyEditor to set up the tab layout via FTabManager::NewLayoutIAssetEditorInstanceIAssetEditorInstance* Editor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()
->FindEditorForAsset(MyAsset, /*bFocusIfOpen=*/false);
if (Editor) Editor->FocusWindow(MyAsset); // CloseWindow(EAssetEditorCloseReason::AssetEditorHostClosed) — no-arg form deprecated 5.3UFactoryUCLASS()
class UMyDataFactory : public UFactory
{
GENERATED_BODY()
public:
UMyDataFactory()
{
SupportedClass = UMyDataAsset::StaticClass();
bCreateNew = true; // appears in "Add" menu
bEditAfterNew = true; // opens editor after creation
}
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent,
FName InName, EObjectFlags Flags, UObject* Context,
FFeedbackContext* Warn) override
{
return NewObject<UMyDataAsset>(InParent, InClass, InName, Flags);
}
};UCLASS()
class UMyThumbnailRenderer : public UThumbnailRenderer
{
GENERATED_BODY()
virtual void Draw(UObject* Object, int32 X, int32 Y, uint32 Width, uint32 Height,
FRenderTarget* Target, FCanvas* Canvas, bool bAdditionalViewFamily) override;
};
// Register in module startup:
UThumbnailManager::Get().RegisterCustomRenderer(UMyDataAsset::StaticClass(),
UMyThumbnailRenderer::StaticClass());UEditorSubsystemEditorSubsystem.hGEditor->GetEditorSubsystem<T>()UCLASS()
class MYGAMEEDITOR_API UMyEditorSubsystem : public UEditorSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override
{
Super::Initialize(Collection);
FEditorDelegates::PostUndoRedo.AddUObject(this, &UMyEditorSubsystem::OnPostUndoRedo);
}
virtual void Deinitialize() override
{
FEditorDelegates::PostUndoRedo.RemoveAll(this);
Super::Deinitialize();
}
};| Subsystem | Purpose |
|---|---|
| Select, spawn, delete level actors |
| Load, save, duplicate assets |
| Open, save, manage levels |
| Spawn editor utility widget tabs |
FExtendervoid FMyGameEditorModule::RegisterMenus()
{
FToolMenuOwnerScoped OwnerScoped(this);
// Main menu
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");
Menu->FindOrAddSection("MyGame").AddMenuEntry("OpenMyPanel",
FText::FromString("My Panel"), FText::GetEmpty(),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"),
FUIAction(FExecuteAction::CreateLambda([]() {
GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>()
->SpawnAndRegisterTab(/* WidgetBP */nullptr);
})));
// Toolbar button
UToolMenu* TB = UToolMenus::Get()->ExtendMenu(
"LevelEditor.LevelEditorToolBar.PlayToolBar");
TB->FindOrAddSection("MyTools").AddEntry(
FToolMenuEntry::InitToolBarButton("MyBtn",
FUIAction(FExecuteAction::CreateLambda([]() {})),
FText::FromString("My Tool"), FText::GetEmpty(),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Toolbar.Settings")));
}UToolMenus::UnRegisterStartupCallback(this)TryGet()->UnregisterOwner(this)class FMyCommands : public TCommands<FMyCommands>
{
public:
FMyCommands() : TCommands("MyEditor", INVTEXT("My Editor"), NAME_None, FAppStyle::GetAppStyleSetName()) {}
virtual void RegisterCommands() override;
TSharedPtr<FUICommandInfo> OpenPanel;
};
void FMyCommands::RegisterCommands()
{
UI_COMMAND(OpenPanel, "My Panel", "Opens the panel", EUserInterfaceActionType::Button, FInputChord());
}
// In StartupModule: FMyCommands::Register(); bind via CommandList->MapAction(...)
// In ShutdownModule: FMyCommands::Unregister();// UEditorValidatorBase — auto-discovered by Editor > Tools > Validate Assets
UCLASS()
class UMyValidator : public UEditorValidatorBase
{
GENERATED_BODY()
virtual bool CanValidateAsset_Implementation(const FAssetData& InAssetData, UObject* InObject, FDataValidationContext& InContext) const override
{ return InObject && InObject->IsA<UMyDataAsset>(); }
virtual EDataValidationResult ValidateLoadedAsset_Implementation(
const FAssetData& InAssetData, UObject* InAsset, FDataValidationContext& Context) override;
};
// Also runnable via commandlet: UnrealEditor-Cmd -run=DataValidation| Mistake | Fix |
|---|---|
| Editor headers in Runtime modules | Move to Editor module; use |
No | Always pair register/unregister; crashes Live Coding reload |
| Raw pointer capture in Slate lambdas | Capture as |
| Use |
| Editor-only; will not exist in packaged builds |
| Use only when layout structure changes; use handles/attributes for values |
| Crashes on plugin reload |
.Build.csWITH_EDITOR