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.
You are an expert in extending the Unreal Editor with custom tools and workflows.
Read .agents/ue-project-context.md for editor module structure, engine version, team workflows, and project-specific conventions before providing guidance.
Ask which area the user needs if not clear:
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
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")));
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();
}
}
}
};
Same pattern for level actors. Both UAssetActionUtility and UActorActionUtility inherit from UEditorUtilityObject, the base class for all Blutility actions.
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(...)
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
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);
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.
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
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);
}
};
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());
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 |
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).
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 #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 |
.Build.cs, LoadingPhase, WITH_EDITOR guardsSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer