UE Editor Tools
You are an expert in extending the Unreal Editor with custom tools and workflows.
Context
Read .agents/ue-project-context.md for editor module structure, engine version, team workflows, and project-specific conventions before providing guidance.
Before You Start
Ask which area the user needs if not clear:
- Editor Utility Widget — UMG panel run from editor right-click
- Blutility — UAssetActionUtility or UActorActionUtility scripted actions
- Detail Customization — Custom property panel (IDetailCustomization / IPropertyTypeCustomization)
- Custom Editor Mode — Viewport mode with specialized interaction (FEdMode)
- Asset Type Actions — Content Browser integration for a custom asset type
- Editor Subsystem — Editor-lifetime singleton (UEditorSubsystem)
- Menu / Toolbar Extension — UToolMenus additions to main menu, toolbars, context menus
Editor Module Setup
All editor-extending code must live in a module with "Type": "Editor". Never put UnrealEd / PropertyEditor includes in a Runtime module without #if WITH_EDITOR guards.
{ "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"
});
Module skeleton — every registration in StartupModule must be mirrored in ShutdownModule:
IMPLEMENT_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
Editor Utility Widgets
UEditorUtilityWidget (from EditorUtilityWidget.h) extends UUserWidget for editor-only UMG panels. Create as a Blueprint subclass (right-click Content Browser > Editor Utilities > Editor Utility Widget). Run: right-click the widget asset > Run Editor Utility Widget. Or subclass in C++:
#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());
}
}
}
Key UEditorUtilityLibrary functions (from EditorUtilityLibrary.h):
| Function | Purpose |
|---|---|
GetSelectedAssets() | Currently selected Content Browser assets |
GetSelectedAssetsOfClass(UClass*) | Filter selection by class |
UEditorAssetLibrary::DeleteAsset(Path) | Delete asset; RenameAsset for move/rename |
GetSelectionSet() | Selected level actors |
SyncBrowserToFolders(TArray<FString>) | Sync content browser view |
Content browser filters: Use FARFilter with IAssetRegistry::GetAssets for programmatic asset queries by class, path, or tags.
Open a widget programmatically:
GEditor->GetEditorSubsystem<UEditorUtilitySubsystem>()
->SpawnAndRegisterTab(LoadObject<UEditorUtilityWidgetBlueprint>(
nullptr, TEXT("/Game/EditorWidgets/BP_MyTool")));
Blutility: Scripted Actions
UAssetActionUtility — Asset Right-Click Actions
Any UFUNCTION(CallInEditor) on a UAssetActionUtility subclass appears in the Content Browser context menu. Set SupportedClasses in Class Defaults to filter by asset type.
#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();
}
}
}
};
UActorActionUtility — Actor Right-Click Actions
Same pattern for level actors. Both UAssetActionUtility and UActorActionUtility inherit from UEditorUtilityObject, the base class for all Blutility actions.
Detail Customizations
Class Customization — IDetailCustomization
class 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(...)
Struct Customization — IPropertyTypeCustomization
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
Custom Editor Modes
FEdMode (from EdMode.h) provides specialized viewport interaction. Register globally; only one active per viewport at a time.
// 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);
}
Register/unregister in module lifecycle:
// StartupModule
FEditorModeRegistry::Get().RegisterMode<FMyEditorMode>(
FMyEditorMode::EM_MyMode, FText::FromString("My Mode"), FSlateIcon(), true);
// ShutdownModule
FEditorModeRegistry::Get().UnregisterMode(FMyEditorMode::EM_MyMode);
Asset Type Actions
FAssetTypeActions_Base (from AssetTypeActions_Base.h) controls Content Browser appearance and context menu for custom asset types.
class 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);
}
};
Register in StartupModule, keep reference, unregister in ShutdownModule.
FAssetEditorToolkit — Tab-Based Asset Editor Window
class 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::NewLayout
Query open editors programmatically via IAssetEditorInstance:
IAssetEditorInstance* Editor = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()
->FindEditorForAsset(MyAsset, /*bFocusIfOpen=*/false);
if (Editor) Editor->FocusWindow(MyAsset); // CloseWindow(EAssetEditorCloseReason::AssetEditorHostClosed) — no-arg form deprecated 5.3
Asset Factories
UFactory subclasses allow the Content Browser's "Add" menu to create new custom assets:
UCLASS()
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);
}
};
Custom Thumbnails
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());
Editor Subsystems
UEditorSubsystem (from EditorSubsystem.h) — editor-lifetime singletons, auto-discovered (no registration needed). Access via GEditor->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();
}
};
Built-in subsystems:
| Subsystem | Purpose |
|---|---|
UEditorActorSubsystem | Select, spawn, delete level actors |
UEditorAssetSubsystem | Load, save, duplicate assets |
ULevelEditorSubsystem | Open, save, manage levels |
UEditorUtilitySubsystem | Spawn editor utility widget tabs |
Menu and Toolbar Extensions (UToolMenus)
UToolMenus (UE5+) replaces FExtender. Register inside the startup callback so Slate is ready:
void 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")));
}
Always unregister: UToolMenus::UnRegisterStartupCallback(this) + TryGet()->UnregisterOwner(this).
Command Registration — TCommands Pattern
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();
Data Validation
// 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
Common Mistakes
| Mistake | Fix |
|---|---|
| Editor headers in Runtime modules | Move to Editor module; use #if WITH_EDITOR for runtime-side editor hooks |
No UnregisterCustomClassLayout in ShutdownModule | Always pair register/unregister; crashes Live Coding reload |
| Raw pointer capture in Slate lambdas | Capture as TWeakPtr; pin before use |
LoadingPhase: Default for editor extension module | Use PostEngineInit for modules registering menus or customizations |
UEditorUtilityWidget referenced from Runtime | Editor-only; will not exist in packaged builds |
ForceRefreshDetails() on every value change | Use only when layout structure changes; use handles/attributes for values |
RegisterMode without UnregisterMode | Crashes on plugin reload |
Related Skills
- ue-ui-umg-slate — Slate widget fundamentals (SNew, TAttribute, FReply, SBox, SHorizontalBox)
- ue-module-build-system — Editor module
.Build.cs, LoadingPhase,WITH_EDITORguards - ue-data-assets-tables — Custom UDataAsset types that need asset editors and type actions
- ue-cpp-foundations — UPROPERTY, UFUNCTION, UObject reflection system