diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9ffe6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.ignore +ToolExample.sln +ToolExample.code-workspace +.vs/ +.vscode/ +.idea/ +Binaries/ +Intermediate/ +Saved/ +.idea/ + +Config/DefaultEditorUserSettings.ini +.vsconfig +Config/DefaultInput.ini +.obsidian/ diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index 5809f22..88a43de 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -1,4 +1,5 @@ [URL] + [/Script/EngineSettings.GameMapsSettings] EditorStartupMap= GameDefaultMap= @@ -51,4 +52,17 @@ AsyncSceneSmoothingFactor=0.990000 InitialAverageFrameRate=0.016667 PhysXTreeRebuildRate=10 +[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] +bEnablePlugin=True +bAllowNetworkConnection=True +SecurityToken=6607BEA0438D48653A04889817D4749C +bIncludeInShipping=False +bAllowExternalStartInShipping=False +bCompileAFSProject=False +bUseCompression=False +bLogFiles=False +bReportStats=False +ConnectionType=USBOnly +bUseManualIPAddress=False +ManualIPAddress= diff --git a/LICENSE b/LICENSE.md similarity index 89% rename from LICENSE rename to LICENSE.md index af44ede..08aa7c7 100644 --- a/LICENSE +++ b/LICENSE.md @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2019 Eric Zhang +Copyright (c) 2019 Eric Zhang +This code includes modifications by Scott Kirvan. Modifications (c) Scott Kirvan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..2584361 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# [ScottKirvan/ToolExample](https://github.com/ScottKirvan/ToolExample) +ToolExample contains UX/UI C/C++ examples for adding tools and UI elements to the [Unreal Engine](https://www.unrealengine.com) editor - which means, these are editor tools for the artist, and not an example of how to write the runtime game code that a player would interact with. + +![Splash Image](docs/images/splash.png) + +If you're looking for an example to show how to add an item to the existing menu, or a new edit mode, or how to create a dockable window, this is a really thorough, [step-by-step example](https://lxjk.github.io/2019/10/01/How-to-Make-Tools-in-U-E.html) that starts from an empty code project, immediately jumps into the base modules needed, and then clearly walks through the steps to code up various UI elements, hooking them to functionality as you go. + +# Features +These examples demonstrate how to implement the following: +- adding modules +- adding menus to the editor interface with: + - sections + - menu items + - sub menus + - adding custom widgets and controls (button and text input) to the menu +- adding a dockable tab Window +- custom details panel +- defining and adding your own custom asset data type (data factory) +- importing/reimporting custom data +- custom editor mode +- custom editor viewport widget +- viewport object context sensitive right-click & menus +- custom project settings/preferences +- additional tricks and tips + +This repo is an updated fork of [Eric Zhang's](https://github.com/lxjk) original [UE4 project](https://github.com/lxjk/ToolExample). The excellent step-by-step documentation can be found here: [How to Make Tools in UE4](https://lxjk.github.io/2019/10/01/How-to-Make-Tools-in-U-E.html) I've included a stripped down version of that tutorial in this repo in case anything happens to that URL in the future. + +This tutorial has become the most valuable one I've run across in my Unreal C++ journey. It got me started in an area of programming in Unreal that I found pretty impenetrable at first, and it has continued to be a reference point that I return to frequently whenever I'm thinking I want some new functionality (or just some easier-to-get-at functionality) inside Unreal. My deepest gratitude to Eric for his contribution here. + +## Branches +- master + - current development branch - this will likely have the most recent code - note: this branch does work with UE 5.2 Preview 1 +- 5.1 + - UE 5.1 Compatible + - There was some header changes, a new module dependency, and class FEditorStyle was deprecated and replaced with FAppStyle. + - I tested this very briefly, but it all looks good! +- 4.26 + - UE 4.26 Compatible. + - Example Editor Mode is working in this version, so I'm not going to bother trying to figure out what broke in 4.25. If anyone takes this on as a project, please toss up a Pull Request. + - initial branch source is identical to 4.24/4.25 +- 4.25 + - UE 4.25 Compatible. + - builds and runs, but the Example Editor Mode isn't working. It shows up in the Modes list, but the UI never gets built when selected. I didn't see any errors or warnings anywhere. + - NOTE: The editor mode issue works in 4.26 without any code changes. + - as it stands, this source is identical to the 4.24 branch +- 4.24 + - UE 4.24 Compatible. + - updates build.cs to V2 target. + - implements [IWYU](https://docs.unrealengine.com/en-US/ProductionPipelines/BuildTools/UnrealBuildTool/IWYU/index.html) standard. +- 4.23 + - original fork from [lxjk/ToolExample](https://github.com/lxjk/ToolExample). + - UE 4.23 compatible. + +## Contributing +Do you know how to create a UI tool that would be good to include in this example? Interested in contributing with writing or anything else? Did you find a bug?! Please help out! Check the issues link to see the kinds of things that might be fun to tackle. + +The best way to contribute would be with [Pull Requests (PR)](https://github.com/ScottKirvan/ToolExample/pulls): Fork this repository, make your changes, and submit a New Pull Request that can be reviewed and rolled back in. + +If you don't want to do the Write/PR process yourself, but would still like to contribute, just use the [Issues](https://github.com/ScottKirvan/ToolExample/issues) link above to report bugs or request something new you'd like to see. + +## System Requirements +- Computer capable of Unreal Engine development + - Epic Recommends [this.](https://docs.unrealengine.com/5.1/en-US/hardware-and-software-specifications-for-unreal-engine/) + - The typical system used by developers at Epic Games looks like this: + - Windows 10 64-bit (Version 20H2) + - 64 GB RAM + - 256 GB SSD (OS Drive) + - 2 TB SSD (Data Drive) + - NVIDIA GeForce RTX 2080 SUPER + - Xoreax Incredibuild (Dev Tools Package) + - Six-Core Xeon E5-2643 @ 3.4GHz + - I get by with a 16 core AMD Ryzen 9, Windows 11, MSDev2022, and 40 GB RAM on my gaming laptop. +- Unreal Engine 5.5 + - This repo also supports older versions, just download/clone the [branch](https://github.com/ScottKirvan/ToolExample/branches) you need. +- Microsoft Visual Studio 2022 + - Epic still recommends the 2019 version, but I'm using 2022 - get the free-to-use [Community version here](https://visualstudio.microsoft.com/vs/community/). + - This repo supports older versions, just clone the branch you need. +- Microsoft Visual Studio 2019 + - the 2022 version works as well, but you have to explicitly tell unreal engine to use 2022 (`Edit -> Editor Preferences... -> Source Code -> Source Code Editor`). + +## Getting Started With Local Development +Clone or download this repo. If you have Unreal and Visual Studio properly installed, double clicking the `uproject` file should launch Unreal, which will recognize it as a code project and build everything automatically. + +I highly recommend following the Tutorial and using this code as reference. If you hit an error, look at this code to see what may have changed. + +A couple of other good resources for getting started: +- [Setting Up Visual Studio for EU5.1](https://docs.unrealengine.com/5.1/en-US/setting-up-visual-studio-development-environment-for-cplusplus-projects-in-unreal-engine/) +- [UE5.1 Programming Quickstart](https://docs.unrealengine.com/5.1/en-US/unreal-engine-cpp-quick-start/) + +## Support/Contact +- Feel free to reach out to me on the [Unreal Slackers](https://discord.gg/unreal-slackers) discord. I'm @Fragmanget_. There is also a ton of other Unreal programmers up there, so if I'm not around to help, someone else be able to get you going. +- You can also reach me on my personal [Discord Server](https://discord.gg/TSKHvVFYxB) (@cptvideo), via [LinkedIn](https://www.linkedin.com/in/scottkirvan/), or email. + +## Submit a Feature Request +Use the [Issues](https://github.com/ScottKirvan/ToolExample/issues) link, above. Thanks! + +## Credits +A huge thank you to [Eric Zhang](https://github.com/lxjk) for the work he put into the [original tutorial project](https://lxjk.github.io/2019/10/01/How-to-Make-Tools-in-U-E.html). + +Contributors: +[Eric Zhang](https://github.com/lxjk) (OG!) +[Scott Kirvan](https://github.com/ScottKirvan) (2021-present) +[Razdvizh](https://github.com/Razdvizh) (2023) +[duyaokun](https://github.com/duyaokun) (2024) +You! (Future!) + +_ToolExample is licensed under the [MIT License](LICENSE.md)._ diff --git a/Source/ToolExampleEditor.Target.cs b/Source/ToolExampleEditor.Target.cs index 6f9eb80..424f917 100644 --- a/Source/ToolExampleEditor.Target.cs +++ b/Source/ToolExampleEditor.Target.cs @@ -8,8 +8,10 @@ public class ToolExampleEditorTarget : TargetRules public ToolExampleEditorTarget(TargetInfo Target) : base(Target) { Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V5; + IncludeOrderVersion = EngineIncludeOrderVersion.Latest; - ExtraModuleNames.AddRange( new string[] { "ToolExample" } ); + ExtraModuleNames.AddRange( new string[] { "ToolExample" } ); ExtraModuleNames.AddRange( new string[] { "ToolExampleEditor" }); } } diff --git a/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.cpp b/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.cpp index 59ab7e1..c6c97b5 100644 --- a/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.cpp +++ b/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.cpp @@ -1,49 +1,16 @@ -#include "ToolExampleEditor/ToolExampleEditor.h" #include "ExampleDataFactory.h" -#include "CustomDataType/ExampleData.h" +#include "ToolExampleEditor/ToolExampleEditor.h" +#include "ToolExample/CustomDataType/ExampleData.h" UExampleDataFactory::UExampleDataFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { - Formats.Add(TEXT("xmp;Example Data")); SupportedClass = UExampleData::StaticClass(); - bCreateNew = false; // turned off for import - bEditAfterNew = false; // turned off for import - bEditorImport = true; - bText = true; + bCreateNew = true; + bEditAfterNew = true; } UObject* UExampleDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) { UExampleData* NewObjectAsset = NewObject(InParent, Class, Name, Flags | RF_Transactional); return NewObjectAsset; -} - -UObject* UExampleDataFactory::FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn) -{ - FEditorDelegates::OnAssetPreImport.Broadcast(this, InClass, InParent, InName, Type); - - // if class type or extension doesn't match, return - if (InClass != UExampleData::StaticClass() || - FCString::Stricmp(Type, TEXT("xmp")) != 0) - return nullptr; - - UExampleData* Data = CastChecked(NewObject(InParent, InName, Flags)); - MakeExampleDataFromText(Data, Buffer, BufferEnd); - - // save the source file path - Data->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, Data->GetOutermost()); - - FEditorDelegates::OnAssetPostImport.Broadcast(this, Data); - - return Data; -} - -bool UExampleDataFactory::FactoryCanImport(const FString& Filename) -{ - return FPaths::GetExtension(Filename).Equals(TEXT("xmp")); -} - -void UExampleDataFactory::MakeExampleDataFromText(class UExampleData* Data, const TCHAR*& Buffer, const TCHAR* BufferEnd) -{ - Data->ExampleString = Buffer; } \ No newline at end of file diff --git a/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.h b/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.h index e037ed3..137d2e2 100644 --- a/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.h +++ b/Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.h @@ -1,4 +1,5 @@ #pragma once + #include "UnrealEd.h" #include "ExampleDataFactory.generated.h" @@ -9,10 +10,5 @@ class UExampleDataFactory : public UFactory public: // Begin UFactory Interface virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn) override; - virtual bool FactoryCanImport(const FString& Filename) override; // End UFactory Interface - - // helper function - static void MakeExampleDataFromText(class UExampleData* Data, const TCHAR*& Buffer, const TCHAR* BufferEnd); }; \ No newline at end of file diff --git a/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.cpp b/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.cpp index 167c2db..2a5851d 100644 --- a/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.cpp +++ b/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.cpp @@ -1,6 +1,7 @@ -#include "ToolExampleEditor/ToolExampleEditor.h" #include "ExampleDataTypeActions.h" -#include "CustomDataType/ExampleData.h" +#include "ToolExampleEditor/ToolExampleEditor.h" +#include "ToolExample/CustomDataType/ExampleData.h" +#include "XMPAssetTypeActions_Base.h" FExampleDataTypeActions::FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory) @@ -42,15 +43,3 @@ void FExampleDataTypeActions::GetActions(const TArray& InObjects, FMen ) ); } - -void FExampleDataTypeActions::ExecuteReimport(TArray> Objects) -{ - for (auto ObjIt = Objects.CreateConstIterator(); ObjIt; ++ObjIt) - { - auto Object = (*ObjIt).Get(); - if (Object) - { - FReimportManager::Instance()->Reimport(Object, /*bAskForNewFileIfMissing=*/true); - } - } -} \ No newline at end of file diff --git a/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.h b/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.h index 797d749..8030122 100644 --- a/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.h +++ b/Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.h @@ -1,10 +1,10 @@ #pragma once -#include "AssetTypeActions_Base.h" +#include "XMPAssetTypeActions_Base.h" class UExampleData; -class FExampleDataTypeActions : public FAssetTypeActions_Base +class FExampleDataTypeActions : public FXMPAssetTypeActions_Base { public: FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory); @@ -18,8 +18,6 @@ class FExampleDataTypeActions : public FAssetTypeActions_Base virtual void GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) override; // End of IAssetTypeActions interface - void ExecuteReimport(TArray> Objects); - private: EAssetTypeCategories::Type MyAssetCategory; }; diff --git a/Source/ToolExampleEditor/CustomDataType/ReimportExampleDataFactory.cpp b/Source/ToolExampleEditor/CustomDataType/ReimportExampleDataFactory.cpp index 2f87497..4e5be33 100644 --- a/Source/ToolExampleEditor/CustomDataType/ReimportExampleDataFactory.cpp +++ b/Source/ToolExampleEditor/CustomDataType/ReimportExampleDataFactory.cpp @@ -1,7 +1,7 @@ -#include "ToolExampleEditor/ToolExampleEditor.h" #include "ReimportExampleDataFactory.h" +#include "ToolExampleEditor/ToolExampleEditor.h" #include "ExampleDataFactory.h" -#include "CustomDataType/ExampleData.h" +#include "ToolExample/CustomDataType/ExampleData.h" bool UReimportExampleDataFactory::CanReimport(UObject* Obj, TArray& OutFilenames) { @@ -44,9 +44,7 @@ EReimportResult::Type UReimportExampleDataFactory::Reimport(UObject* Obj) const TCHAR* Ptr = *Data; ExampleData->Modify(); ExampleData->MarkPackageDirty(); - - UExampleDataFactory::MakeExampleDataFromText(ExampleData, Ptr, Ptr + Data.Len()); - + ExampleData->ExampleString = Ptr; // save the source file path and timestamp ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, ExampleData->GetOutermost()); } diff --git a/Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.cpp b/Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.cpp new file mode 100644 index 0000000..54187c0 --- /dev/null +++ b/Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.cpp @@ -0,0 +1,64 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "XMPAssetTypeActions_Base.h" +#include "ToolExample/CustomDataType/ExampleData.h" +#include "EditorReimportHandler.h" +#include "EditorFramework/AssetImportData.h" +#include "Misc/FileHelper.h" + +bool FXMPAssetTypeActions_Base::CanReimport(UObject* Obj, TArray& OutFilenames) +{ + UExampleData* ExampleData = Cast(Obj); + if (ExampleData) + { + OutFilenames.Add(UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost())); + return true; + } + return false; +} + +void FXMPAssetTypeActions_Base::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) +{ + check(NewReimportPaths.IsValidIndex(0)); + if (UExampleData* ExampleData = Cast(Obj)) + { + ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(NewReimportPaths[0], ExampleData->GetOutermost()); + } +} + +EReimportResult::Type FXMPAssetTypeActions_Base::Reimport(UObject* Obj) +{ + UExampleData* ExampleData = Cast(Obj); + if (!ExampleData) + { + return EReimportResult::Failed; + } + + const FString Filename = UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost()); + + FString Data; + if (FFileHelper::LoadFileToString(Data, *Filename)) + { + const TCHAR* Ptr = *Data; + ExampleData->Modify(); + ExampleData->MarkPackageDirty(); + + ExampleData->ExampleString = Ptr; + ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(Filename, ExampleData->GetOutermost()); + } + + return EReimportResult::Succeeded; +} + +void FXMPAssetTypeActions_Base::ExecuteReimport(TArray> Objects) +{ + for (auto ObjIt = Objects.CreateConstIterator(); ObjIt; ++ObjIt) + { + auto Object = (*ObjIt).Get(); + if (Object) + { + Reimport(Object); + } + } +} diff --git a/Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.h b/Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.h new file mode 100644 index 0000000..e00421a --- /dev/null +++ b/Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.h @@ -0,0 +1,27 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "AssetTypeActions_Base.h" +#include "EditorReimportHandler.h" + +class UExampleData; + +class FXMPAssetTypeActions_Base : public FAssetTypeActions_Base, public FReimportHandler +{ +public: + //It won't be shown in the content browser, it is a proxy for an `Example Data` asset + virtual FColor GetTypeColor() const override { return FColor(0, 0, 0); } + virtual bool HasActions(const TArray& InObjects) const override { return true; } + virtual uint32 GetCategories() override { return EAssetTypeCategories::Misc; } + virtual bool IsImportedAsset() const override { return true; } + + virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override; + virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override; + virtual EReimportResult::Type Reimport(UObject* Obj) override; + +protected: + void ExecuteReimport(TArray> Objects); + +}; diff --git a/Source/ToolExampleEditor/CustomDataType/XMPImportFactory.cpp b/Source/ToolExampleEditor/CustomDataType/XMPImportFactory.cpp new file mode 100644 index 0000000..bdd03f5 --- /dev/null +++ b/Source/ToolExampleEditor/CustomDataType/XMPImportFactory.cpp @@ -0,0 +1,43 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "XMPImportFactory.h" +#include "ToolExample/CustomDataType/ExampleData.h" +#include "EditorFramework/AssetImportData.h" + +#define LOCTEXT_NAMESPACE "XMPImportFactory" + +UXMPImportFactory::UXMPImportFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) +{ + SupportedClass = UExampleData::StaticClass(); + bCreateNew = false; + bEditorImport = true; + bEditAfterNew = true; + bText = true; + + Formats.Add(TEXT("xmp;Example Data")); +} + +FText UXMPImportFactory::GetDisplayName() const +{ + return LOCTEXT("XMPImportFactoryDisplayName", "Example Data"); +} + +UObject* UXMPImportFactory::FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn, bool& bOutOperationCanceled) +{ + GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type); + + UExampleData* ExampleData = CastChecked(NewObject(InParent, InName, Flags)); + const int32 NumChars = BufferEnd - Buffer; + ExampleData->ExampleString = FString(NumChars, Buffer); + ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, ExampleData->GetOutermost()); + + GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, ExampleData); + + return ExampleData; +} + +bool UXMPImportFactory::FactoryCanImport(const FString& Filename) +{ + return FPaths::GetExtension(Filename).Equals(TEXT("xmp")); +} diff --git a/Source/ToolExampleEditor/CustomDataType/XMPImportFactory.h b/Source/ToolExampleEditor/CustomDataType/XMPImportFactory.h new file mode 100644 index 0000000..13b059f --- /dev/null +++ b/Source/ToolExampleEditor/CustomDataType/XMPImportFactory.h @@ -0,0 +1,21 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "Factories/ImportSettings.h" +#include "XMPImportFactory.generated.h" + +UCLASS(customconstructor) +class UXMPImportFactory : public UFactory +{ + GENERATED_UCLASS_BODY() + +public: + UXMPImportFactory(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + + virtual FText GetDisplayName() const override; + virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn, bool& bOutOperationCanceled) override; + virtual bool FactoryCanImport(const FString& Filename) override; +}; diff --git a/Source/ToolExampleEditor/DetailsCustomization/ExampleActorDetails.cpp b/Source/ToolExampleEditor/DetailsCustomization/ExampleActorDetails.cpp index e277bd5..4372d6b 100644 --- a/Source/ToolExampleEditor/DetailsCustomization/ExampleActorDetails.cpp +++ b/Source/ToolExampleEditor/DetailsCustomization/ExampleActorDetails.cpp @@ -1,8 +1,8 @@ - -#include "ToolExampleEditor/ToolExampleEditor.h" #include "ExampleActorDetails.h" +#include "ToolExampleEditor/ToolExampleEditor.h" +#include "ToolExample/DetailsCustomization/ExampleActor.h" -#include "DetailsCustomization/ExampleActor.h" +//#include "DetailsCustomization/ExampleActor.h" TSharedRef FExampleActorDetails::MakeInstance() { @@ -35,7 +35,7 @@ void FExampleActorDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) .VAlign(VAlign_Center) [ SNew(SCheckBox) - .Style(FEditorStyle::Get(), "RadioButton") + .Style(FAppStyle::Get(), "RadioButton") .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 1) .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 1) [ @@ -48,7 +48,7 @@ void FExampleActorDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) .VAlign(VAlign_Center) [ SNew(SCheckBox) - .Style(FEditorStyle::Get(), "RadioButton") + .Style(FAppStyle::Get(), "RadioButton") .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 2) .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 2) [ diff --git a/Source/ToolExampleEditor/EditorMode/ExampleEdMode.cpp b/Source/ToolExampleEditor/EditorMode/ExampleEdMode.cpp index cfef2f8..a42d1af 100644 --- a/Source/ToolExampleEditor/EditorMode/ExampleEdMode.cpp +++ b/Source/ToolExampleEditor/EditorMode/ExampleEdMode.cpp @@ -1,20 +1,18 @@ +#include "ExampleEdMode.h" #include "ToolExampleEditor/ToolExampleEditor.h" -#include "Editor/UnrealEd/Public/Toolkits/ToolkitManager.h" +#include "Toolkits/ToolkitManager.h" #include "ScopedTransaction.h" #include "ExampleEdModeToolkit.h" -#include "ExampleEdMode.h" -#include "EditorMode/ExampleTargetPoint.h" +#include "ToolExample/EditorMode/ExampleTargetPoint.h" class ExampleEditorCommands : public TCommands { public: - ExampleEditorCommands() : TCommands - ( - "ExampleEditor", // Context name for fast lookup - FText::FromString(TEXT("Example Editor")), // context name for displaying - NAME_None, // Parent - FEditorStyle::GetStyleSetName() - ) + ExampleEditorCommands() : TCommands( + "ExampleEditor", // Context name for fast lookup + FText::FromString(TEXT("Example Editor")), // context name for displaying + NAME_None, // Parent + FAppStyle::GetAppStyleSetName()) { } @@ -46,7 +44,7 @@ FExampleEdMode::~FExampleEdMode() void FExampleEdMode::MapCommands() { - const auto& Commands = ExampleEditorCommands::Get(); + const auto &Commands = ExampleEditorCommands::Get(); ExampleEdModeActions->MapAction( Commands.DeletePoint, @@ -57,7 +55,7 @@ void FExampleEdMode::MapCommands() void FExampleEdMode::Enter() { FEdMode::Enter(); - + if (!Toolkit.IsValid()) { Toolkit = MakeShareable(new FExampleEdModeToolkit); @@ -75,26 +73,26 @@ void FExampleEdMode::Exit() { FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef()); Toolkit.Reset(); - + FEdMode::Exit(); } -void FExampleEdMode::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) +void FExampleEdMode::Render(const FSceneView *View, FViewport *Viewport, FPrimitiveDrawInterface *PDI) { const FColor normalColor(200, 200, 200); const FColor selectedColor(255, 128, 0); - UWorld* World = GetWorld(); + UWorld *World = GetWorld(); for (TActorIterator It(World); It; ++It) { - AExampleTargetPoint* actor = (*It); + AExampleTargetPoint *actor = (*It); if (actor) { FVector actorLoc = actor->GetActorLocation(); for (int i = 0; i < actor->Points.Num(); ++i) { bool bSelected = (actor == currentSelectedTarget && i == currentSelectedIndex); - const FColor& color = bSelected ? selectedColor : normalColor; + const FColor &color = bSelected ? selectedColor : normalColor; // set hit proxy and draw PDI->SetHitProxy(new HExamplePointProxy(actor, i)); PDI->DrawPoint(actor->Points[i], color, 15.f, SDPG_Foreground); @@ -107,7 +105,7 @@ void FExampleEdMode::Render(const FSceneView* View, FViewport* Viewport, FPrimit FEdMode::Render(View, Viewport, PDI); } -bool FExampleEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) +bool FExampleEdMode::HandleClick(FEditorViewportClient *InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) { bool isHandled = false; @@ -116,8 +114,8 @@ bool FExampleEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitPr if (HitProxy->IsA(HExamplePointProxy::StaticGetType())) { isHandled = true; - HExamplePointProxy* examplePointProxy = (HExamplePointProxy*)HitProxy; - AExampleTargetPoint* actor = Cast(examplePointProxy->RefObject); + HExamplePointProxy *examplePointProxy = (HExamplePointProxy *)HitProxy; + AExampleTargetPoint *actor = Cast(examplePointProxy->RefObject); int32 index = examplePointProxy->Index; if (actor && index >= 0 && index < actor->Points.Num()) { @@ -143,13 +141,13 @@ bool FExampleEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitPr return isHandled; } -bool FExampleEdMode::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) +bool FExampleEdMode::InputDelta(FEditorViewportClient *InViewportClient, FViewport *InViewport, FVector &InDrag, FRotator &InRot, FVector &InScale) { if (InViewportClient->GetCurrentWidgetAxis() == EAxisList::None) { return false; } - + if (HasValidSelection()) { if (!InDrag.IsZero()) @@ -163,7 +161,7 @@ bool FExampleEdMode::InputDelta(FEditorViewportClient* InViewportClient, FViewpo return false; } -bool FExampleEdMode::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) +bool FExampleEdMode::InputKey(FEditorViewportClient *ViewportClient, FViewport *Viewport, FKey Key, EInputEvent Event) { bool isHandled = false; @@ -175,7 +173,7 @@ bool FExampleEdMode::InputKey(FEditorViewportClient* ViewportClient, FViewport* return isHandled; } -TSharedPtr FExampleEdMode::GenerateContextMenu(FEditorViewportClient* InViewportClient) const +TSharedPtr FExampleEdMode::GenerateContextMenu(FEditorViewportClient *InViewportClient) const { FMenuBuilder MenuBuilder(true, NULL); @@ -186,8 +184,8 @@ TSharedPtr FExampleEdMode::GenerateContextMenu(FEditorViewportClient* I // add label for point index TSharedRef LabelWidget = SNew(STextBlock) - .Text(FText::FromString(FString::FromInt(currentSelectedIndex))) - .ColorAndOpacity(FLinearColor::Green); + .Text(FText::FromString(FString::FromInt(currentSelectedIndex))) + .ColorAndOpacity(FLinearColor::Green); MenuBuilder.AddWidget(LabelWidget, FText::FromString(TEXT("Point Index: "))); MenuBuilder.AddMenuSeparator(); // add delete point entry @@ -224,9 +222,9 @@ FVector FExampleEdMode::GetWidgetLocation() const return FEdMode::GetWidgetLocation(); } -AExampleTargetPoint* GetSelectedTargetPointActor() +AExampleTargetPoint *GetSelectedTargetPointActor() { - TArray selectedObjects; + TArray selectedObjects; GEditor->GetSelectedActors()->GetSelectedObjects(selectedObjects); if (selectedObjects.Num() == 1) { @@ -237,13 +235,13 @@ AExampleTargetPoint* GetSelectedTargetPointActor() void FExampleEdMode::AddPoint() { - AExampleTargetPoint* actor = GetSelectedTargetPointActor(); + AExampleTargetPoint *actor = GetSelectedTargetPointActor(); if (actor) { const FScopedTransaction Transaction(FText::FromString("Add Point")); // add new point, slightly in front of camera - FEditorViewportClient* client = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient(); + FEditorViewportClient *client = (FEditorViewportClient *)GEditor->GetActiveViewport()->GetClient(); FVector newPoint = client->GetViewLocation() + client->GetViewRotation().Vector() * 50.f; actor->Modify(); actor->Points.Add(newPoint); @@ -280,7 +278,7 @@ bool FExampleEdMode::HasValidSelection() const return currentSelectedTarget.IsValid() && currentSelectedIndex >= 0 && currentSelectedIndex < currentSelectedTarget->Points.Num(); } -void FExampleEdMode::SelectPoint(AExampleTargetPoint* actor, int32 index) +void FExampleEdMode::SelectPoint(AExampleTargetPoint *actor, int32 index) { currentSelectedTarget = actor; currentSelectedIndex = index; diff --git a/Source/ToolExampleEditor/EditorMode/ExampleEdMode.h b/Source/ToolExampleEditor/EditorMode/ExampleEdMode.h index 0652dd7..74c142a 100644 --- a/Source/ToolExampleEditor/EditorMode/ExampleEdMode.h +++ b/Source/ToolExampleEditor/EditorMode/ExampleEdMode.h @@ -1,6 +1,7 @@ #pragma once #include "EditorModes.h" +#include "EdMode.h" struct HExamplePointProxy : public HHitProxy { diff --git a/Source/ToolExampleEditor/EditorMode/ExampleEdModeTool.cpp b/Source/ToolExampleEditor/EditorMode/ExampleEdModeTool.cpp index aebf936..d697042 100644 --- a/Source/ToolExampleEditor/EditorMode/ExampleEdModeTool.cpp +++ b/Source/ToolExampleEditor/EditorMode/ExampleEdModeTool.cpp @@ -1,5 +1,5 @@ -#include "ToolExampleEditor/ToolExampleEditor.h" #include "ExampleEdModeTool.h" +#include "ToolExampleEditor/ToolExampleEditor.h" #include "ExampleEdMode.h" #define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__) @@ -31,8 +31,8 @@ void ExampleEdModeTool::RegisterStyleSet() } StyleSet = MakeShareable(new FSlateStyleSet("ExampleEdModeToolStyle")); - StyleSet->SetContentRoot(FPaths::GameDir() / TEXT("Content/EditorResources")); - StyleSet->SetCoreContentRoot(FPaths::GameDir() / TEXT("Content/EditorResources")); + StyleSet->SetContentRoot(FPaths::ProjectDir() / TEXT("Content/EditorResources")); + StyleSet->SetCoreContentRoot(FPaths::ProjectDir() / TEXT("Content/EditorResources")); // Spline editor { diff --git a/Source/ToolExampleEditor/EditorMode/ExampleEdModeToolkit.h b/Source/ToolExampleEditor/EditorMode/ExampleEdModeToolkit.h index 667a11f..1eb1766 100644 --- a/Source/ToolExampleEditor/EditorMode/ExampleEdModeToolkit.h +++ b/Source/ToolExampleEditor/EditorMode/ExampleEdModeToolkit.h @@ -1,6 +1,6 @@ #pragma once -#include "BaseToolkit.h" +#include "Toolkits/BaseToolkit.h" #include "ExampleEdMode.h" #include "SExampleEdModeWidget.h" diff --git a/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.cpp b/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.cpp index 90f778f..06385fd 100644 --- a/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.cpp +++ b/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.cpp @@ -1,6 +1,6 @@ +#include "SExampleEdModeWidget.h" #include "ToolExampleEditor/ToolExampleEditor.h" #include "ExampleEdMode.h" -#include "SExampleEdModeWidget.h" void SExampleEdModeWidget::Construct(const FArguments& InArgs) { diff --git a/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.h b/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.h index 61a766c..263ae60 100644 --- a/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.h +++ b/Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.h @@ -1,6 +1,6 @@ #pragma once -#include "SlateApplication.h" +#include "Framework/APplication/SlateApplication.h" class SExampleEdModeWidget : public SCompoundWidget { diff --git a/Source/ToolExampleEditor/ExampleTabToolBase.h b/Source/ToolExampleEditor/ExampleTabToolBase.h index 0b7a33f..5d3bc01 100644 --- a/Source/ToolExampleEditor/ExampleTabToolBase.h +++ b/Source/ToolExampleEditor/ExampleTabToolBase.h @@ -2,8 +2,8 @@ #include "ToolExampleEditor/ToolExampleEditor.h" #include "ToolExampleEditor/IExampleModuleInterface.h" -#include "TabManager.h" -#include "SDockTab.h" +#include "Framework/Docking/TabManager.h" +#include "Widgets/Docking/SDockTab.h" class FExampleTabToolBase : public IExampleModuleListenerInterface, public TSharedFromThis< FExampleTabToolBase > { @@ -33,7 +33,8 @@ class FExampleTabToolBase : public IExampleModuleListenerInterface, public TShar { FGlobalTabmanager::Get()->PopulateTabSpawnerMenu(menuBuilder, TabName); }; - + + virtual ~FExampleTabToolBase() { }; // this gets rid of a virtual destructor warning, but I'm no sure this is the correct way to handle it - you can also use a pragma to supress the warning protected: FName TabName; FText TabDisplayName; diff --git a/Source/ToolExampleEditor/IExampleModuleInterface.h b/Source/ToolExampleEditor/IExampleModuleInterface.h index c325fa1..e1b0c9b 100644 --- a/Source/ToolExampleEditor/IExampleModuleInterface.h +++ b/Source/ToolExampleEditor/IExampleModuleInterface.h @@ -1,6 +1,6 @@ #pragma once -#include "ModuleManager.h" +#include "Modules/ModuleManager.h" class IExampleModuleListenerInterface diff --git a/Source/ToolExampleEditor/MenuTool/MenuTool.cpp b/Source/ToolExampleEditor/MenuTool/MenuTool.cpp index a4054ba..3d9c806 100644 --- a/Source/ToolExampleEditor/MenuTool/MenuTool.cpp +++ b/Source/ToolExampleEditor/MenuTool/MenuTool.cpp @@ -1,5 +1,5 @@ -#include "ToolExampleEditor/ToolExampleEditor.h" #include "MenuTool.h" +#include "ToolExampleEditor/ToolExampleEditor.h" #include "ScopedTransaction.h" @@ -8,14 +8,13 @@ class MenuToolCommands : public TCommands { public: - MenuToolCommands() : TCommands( - TEXT("MenuTool"), // Context name for fast lookup - FText::FromString("Example Menu tool"), // Context name for displaying - NAME_None, // No parent context - FEditorStyle::GetStyleSetName() // Icon Style Set - ) + TEXT("MenuTool"), // Context name for fast lookup + FText::FromString("Example Menu tool"), // Context name for displaying + NAME_None, // No parent context + FAppStyle::GetAppStyleSetName() // Icon Style Set + ) { } @@ -24,7 +23,6 @@ class MenuToolCommands : public TCommands UI_COMMAND(MenuCommand1, "Menu Command 1", "Test Menu Command 1.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(MenuCommand2, "Menu Command 2", "Test Menu Command 2.", EUserInterfaceActionType::Button, FInputGesture()); UI_COMMAND(MenuCommand3, "Menu Command 3", "Test Menu Command 3.", EUserInterfaceActionType::Button, FInputGesture()); - } public: @@ -35,7 +33,7 @@ class MenuToolCommands : public TCommands void MenuTool::MapCommands() { - const auto& Commands = MenuToolCommands::Get(); + const auto &Commands = MenuToolCommands::Get(); CommandList->MapAction( Commands.MenuCommand1, @@ -53,7 +51,6 @@ void MenuTool::MapCommands() FCanExecuteAction()); } - void MenuTool::OnStartupModule() { CommandList = MakeShareable(new FUICommandList); @@ -70,37 +67,17 @@ void MenuTool::OnShutdownModule() MenuToolCommands::Unregister(); } - void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder) { menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand1); menuBuilder.AddSubMenu( FText::FromString("Sub Menu"), FText::FromString("This is example sub menu"), - FNewMenuDelegate::CreateSP(this, &MenuTool::MakeSubMenu) - ); + FNewMenuDelegate::CreateSP(this, &MenuTool::MakeSubMenu)); // add tag TSharedRef AddTagWidget = - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(SEditableTextBox) - .MinDesiredWidth(50) - .Text(this, &MenuTool::GetTagToAddText) - .OnTextCommitted(this, &MenuTool::OnTagToAddTextCommited) - ] - + SHorizontalBox::Slot() - .AutoWidth() - .Padding(5, 0, 0, 0) - .VAlign(VAlign_Center) - [ - SNew(SButton) - .Text(FText::FromString("Add Tag")) - .OnClicked(this, &MenuTool::AddTag) - ]; + SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[SNew(SEditableTextBox).MinDesiredWidth(50).Text(this, &MenuTool::GetTagToAddText).OnTextCommitted(this, &MenuTool::OnTagToAddTextCommited)] + SHorizontalBox::Slot().AutoWidth().Padding(5, 0, 0, 0).VAlign(VAlign_Center)[SNew(SButton).Text(FText::FromString("Add Tag")).OnClicked(this, &MenuTool::AddTag)]; menuBuilder.AddWidget(AddTagWidget, FText::FromString("")); } @@ -108,7 +85,7 @@ void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder) void MenuTool::MakeSubMenu(FMenuBuilder &menuBuilder) { menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand2); - menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand3); + menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand3); } void MenuTool::MenuCommand1() @@ -133,7 +110,7 @@ FReply MenuTool::AddTag() const FScopedTransaction Transaction(FText::FromString("Add Tag")); for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) { - AActor* Actor = static_cast(*It); + AActor *Actor = static_cast(*It); if (!Actor->Tags.Contains(TagToAdd)) { Actor->Modify(); @@ -150,10 +127,10 @@ FText MenuTool::GetTagToAddText() const return FText::FromName(TagToAdd); } -void MenuTool::OnTagToAddTextCommited(const FText& InText, ETextCommit::Type CommitInfo) +void MenuTool::OnTagToAddTextCommited(const FText &InText, ETextCommit::Type CommitInfo) { FString str = InText.ToString(); - TagToAdd = FName(*str.Trim()); + TagToAdd = FName(*str.TrimStart()); } #undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/ToolExampleEditor/TabTool/TabTool.cpp b/Source/ToolExampleEditor/TabTool/TabTool.cpp index c84de6c..df9ec38 100644 --- a/Source/ToolExampleEditor/TabTool/TabTool.cpp +++ b/Source/ToolExampleEditor/TabTool/TabTool.cpp @@ -1,4 +1,4 @@ - +#include "TabTool.h" //#include "AssetRegistryModule.h" //#include "ScopedTransaction.h" //#include "SDockTab.h" @@ -7,7 +7,6 @@ //#include "SlateApplication.h" #include "ToolExampleEditor/ToolExampleEditor.h" #include "TabToolPanel.h" -#include "TabTool.h" void TabTool::OnStartupModule() { diff --git a/Source/ToolExampleEditor/TabTool/TabToolPanel.cpp b/Source/ToolExampleEditor/TabTool/TabToolPanel.cpp index cbdc82d..0608060 100644 --- a/Source/ToolExampleEditor/TabTool/TabToolPanel.cpp +++ b/Source/ToolExampleEditor/TabTool/TabToolPanel.cpp @@ -1,6 +1,5 @@ - -#include "ToolExampleEditor/ToolExampleEditor.h" #include "TabToolPanel.h" +#include "ToolExampleEditor/ToolExampleEditor.h" void TabToolPanel::Construct(const FArguments& InArgs) { diff --git a/Source/ToolExampleEditor/TabTool/TabToolPanel.h b/Source/ToolExampleEditor/TabTool/TabToolPanel.h index 468082a..9e42f91 100644 --- a/Source/ToolExampleEditor/TabTool/TabToolPanel.h +++ b/Source/ToolExampleEditor/TabTool/TabToolPanel.h @@ -1,23 +1,21 @@ #pragma once -#include "SDockTab.h" -#include "SDockableTab.h" -#include "SDockTabStack.h" -#include "SlateApplication.h" +#include "Widgets/Docking/SDockTab.h" +#include "Framework/Application/SlateApplication.h" #include "TabTool.h" class TabToolPanel : public SCompoundWidget { SLATE_BEGIN_ARGS(TabToolPanel) - {} + { + } SLATE_ARGUMENT(TWeakPtr, Tool) SLATE_END_ARGS() - void Construct(const FArguments& InArgs); + void Construct(const FArguments &InArgs); protected: - TWeakPtr tool; }; \ No newline at end of file diff --git a/Source/ToolExampleEditor/ToolExampleEditor.Build.cs b/Source/ToolExampleEditor/ToolExampleEditor.Build.cs index 2f57162..2377913 100644 --- a/Source/ToolExampleEditor/ToolExampleEditor.Build.cs +++ b/Source/ToolExampleEditor/ToolExampleEditor.Build.cs @@ -7,6 +7,9 @@ public class ToolExampleEditor : ModuleRules { public ToolExampleEditor(ReadOnlyTargetRules Target) : base(Target) { + + //PrivatePCHHeaderFile = "ToolExampleEditor.h"; // TODO - see if adding this back in helps much with normal dev cycles + PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... @@ -46,6 +49,7 @@ public ToolExampleEditor(ReadOnlyTargetRules Target) : base(Target) "Engine", "AppFramework", "SlateCore", + "EditorFramework", "AnimGraph", "UnrealEd", "KismetWidgets", diff --git a/Source/ToolExampleEditor/ToolExampleEditor.cpp b/Source/ToolExampleEditor/ToolExampleEditor.cpp index 5e7b632..5b4b97d 100644 --- a/Source/ToolExampleEditor/ToolExampleEditor.cpp +++ b/Source/ToolExampleEditor/ToolExampleEditor.cpp @@ -5,8 +5,8 @@ #include "TabTool/TabTool.h" #include "EditorMode/ExampleEdModeTool.h" -#include "DetailsCustomization/ExampleActor.h" -#include "DetailsCustomization/ExampleActorDetails.h" +#include "ToolExample/DetailsCustomization/ExampleActor.h" +#include "ToolExampleEditor/DetailsCustomization/ExampleActorDetails.h" #include "CustomDataType/ExampleDataTypeActions.h" diff --git a/ToolExample.uproject b/ToolExample.uproject index a3132ba..d74dfe0 100644 --- a/ToolExample.uproject +++ b/ToolExample.uproject @@ -1,6 +1,6 @@ { "FileVersion": 3, - "EngineAssociation": "4.23", + "EngineAssociation": "5.5", "Category": "", "Description": "", "Modules": [ @@ -14,7 +14,8 @@ "Type": "Editor", "LoadingPhase": "PostEngineInit", "AdditionalDependencies": [ - "Engine" + "Engine", + "UnrealEd" ] } ] diff --git a/docs/How to Make Tools in UE4 - Eric's Blog.html b/docs/How to Make Tools in UE4 - Eric's Blog.html new file mode 100644 index 0000000..986d408 --- /dev/null +++ b/docs/How to Make Tools in UE4 - Eric's Blog.html @@ -0,0 +1,2717 @@ + + + + + +
+ +
+ +

How to Make Tools in UE4

+ by Eric Zhang - original article: https://lxjk.github.io/2019/10/01/How-to-Make-Tools-in-U-E.html + +
+ +
+
+
+
+

This article is based on Unreal 4.17 code base, tested in Unreal 4.23.

+
+
+

This is a step by step tutorial to write tools for your Unreal project. I would assume you are familiar with Unreal already. This is NOT a tutorial for SLATE code, that deserves a tutorial for its own, and there are lots of SLATE example in Unreal already. With that said there will be some basic SLATE code in this tutorial to build UI widget, and I will try to show some different use cases for each example.

+
+
+

The example project is available in https://github.com/lxjk/ToolExample . Right click on the "ToolExample.uproject" and choose Switch Unreal Engine version to link to your engine.

+
+ +
+
+
+

Setup Editor Module

+
+
+

To make proper tools in Unreal it is almost a must to setup a custom editor module first. This will provide you an entry point for you custom tools, and also make sure your tool will not be included other than running in editor.

+
+
+

Here we create a new ToolExample project.

+
+
+

First we want to create a "ToolExampleEditor" folder and add the following files. This will be our new editor module.

+
+
+
+001.png +
+
+
+

IExampleModuleInterface.h

+
+

In this header, we first define IExampleModuleListenerInterface, a convenient interface to provide event when our module starts up or shuts down. Almost all our later tools will need to implement this interface.

+
+
+

Then we define IExampleModuleInterface, this is not necessary if you only have one editor module, but if you have more than that, this will handle event broadcasting for you. +It is required that a module inherit from IModuleInterface, so our interface will inherit from the same class.

+
+
+
IExampleModuleInterface.h
+
+
#include "ModuleManager.h"
+
+class IExampleModuleListenerInterface
+{
+public:
+    virtual void OnStartupModule() {};
+    virtual void OnShutdownModule() {};
+};
+
+class IExampleModuleInterface : public IModuleInterface
+{
+public:
+    void StartupModule() override
+    {
+        if (!IsRunningCommandlet())
+        {
+            AddModuleListeners();
+            for (int32 i = 0; i < ModuleListeners.Num(); ++i)
+            {
+                ModuleListeners[i]->OnStartupModule();
+            }
+        }
+    }
+
+    void ShutdownModule() override
+    {
+        for (int32 i = 0; i < ModuleListeners.Num(); ++i)
+        {
+            ModuleListeners[i]->OnShutdownModule();
+        }
+    }
+
+    virtual void AddModuleListeners() {};
+
+protected:
+    TArray<TSharedRef<IExampleModuleListenerInterface>> ModuleListeners;
+};
+
+
+
+
+

ToolExmampleEditor.Build.cs

+
+

This file you can copy from ToolExample.Build.cs. We added commonly used module names to dependency. Note we add "ToolExample" module here as well.

+
+
+
ToolExmampleEditor.Build.cs
+
+
PublicDependencyModuleNames.AddRange(
+            new string[] {
+                "Core",
+                "Engine",
+                "CoreUObject",
+                "InputCore",
+                "LevelEditor",
+                "Slate",
+                "EditorStyle",
+                "AssetTools",
+                "EditorWidgets",
+                "UnrealEd",
+                "BlueprintGraph",
+                "AnimGraph",
+                "ComponentVisualizers",
+                "ToolExample"
+        }
+        );
+
+
+PrivateDependencyModuleNames.AddRange(
+            new string[]
+            {
+                "Core",
+                "CoreUObject",
+                "Engine",
+                "AppFramework",
+                "SlateCore",
+                "AnimGraph",
+                "UnrealEd",
+                "KismetWidgets",
+                "MainFrame",
+                "PropertyEditor",
+                "ComponentVisualizers",
+                "ToolExample"
+            }
+            );
+
+
+
+
+

ToolExampleEditor.h & ToolExampleEditor.cpp

+
+

Here we define the actual module class, implementing IExampleModuleInterface we defined above. We include headers we need for following sections as well. Make sure the module name you use to get module is the same as the one you pass in IMPLEMENT_GAME_MODULE macro.

+
+
+
ToolExampleEditor.h
+
+
#include "UnrealEd.h"
+#include "SlateBasics.h"
+#include "SlateExtras.h"
+#include "Editor/LevelEditor/Public/LevelEditor.h"
+#include "Editor/PropertyEditor/Public/PropertyEditing.h"
+#include "IAssetTypeActions.h"
+#include "IExampleModuleInterface.h"
+
+class FToolExampleEditor : public IExampleModuleInterface
+{
+public:
+    /** IModuleInterface implementation */
+    virtual void StartupModule() override;
+    virtual void ShutdownModule() override;
+
+    virtual void AddModuleListeners() override;
+
+    static inline FToolExampleEditor& Get()
+    {
+        return FModuleManager::LoadModuleChecked< FToolExampleEditor >("ToolExampleEditor");
+    }
+
+    static inline bool IsAvailable()
+    {
+        return FModuleManager::Get().IsModuleLoaded("ToolExampleEditor");
+    }
+};
+
+
+
+
ToolExampleEditor.cpp
+
+
#include "ToolExampleEditor.h"
+#include "IExampleModuleInterface.h"
+
+IMPLEMENT_GAME_MODULE(FToolExampleEditor, ToolExampleEditor)
+
+void FToolExampleEditor::AddModuleListeners()
+{
+    // add tools later
+}
+
+void FToolExampleEditor::StartupModule()
+{
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+
+

ToolExampleEditor.Target.cs

+
+

We need to modify this file to load our module in Editor mode (Don’t change ToolExample.Target.cs), add the following:

+
+
+
ToolExampleEditor.Target.cs
+
+
ExtraModuleNames.AddRange( new string[] { "ToolExampleEditor" });
+
+
+
+
+

ToolExample.uproject

+
+

Similarly, we need to include our modules here, add the following:

+
+
+
ToolExample.uproject
+
+
{
+    "Name": "ToolExampleEditor",
+    "Type": "Editor",
+    "LoadingPhase": "PostEngineInit",
+    "AdditionalDependencies": [
+        "Engine"
+    ]
+}
+
+
+
+

Now the editor module should be setup properly.

+
+
+
+
+
+

Add Custom Menu

+
+
+

Next we are going to add a custom menu, so we can add widget in the menu to run a command or open up a window.

+
+
+

First we need to add menu extensions related functions in our editor module ToolExampleEditor:

+
+
+
ToolExampleEditor.h
+
+
public:
+    void AddMenuExtension(const FMenuExtensionDelegate &extensionDelegate, FName extensionHook, const TSharedPtr<FUICommandList> &CommandList = NULL, EExtensionHook::Position position = EExtensionHook::Before);
+    TSharedRef<FWorkspaceItem> GetMenuRoot() { return MenuRoot; };
+
+protected:
+    TSharedPtr<FExtensibilityManager> LevelEditorMenuExtensibilityManager;
+    TSharedPtr<FExtender> MenuExtender;
+
+    static TSharedRef<FWorkspaceItem> MenuRoot;
+
+    void MakePulldownMenu(FMenuBarBuilder &menuBuilder);
+    void FillPulldownMenu(FMenuBuilder &menuBuilder);
+
+
+
+

In the cpp file, define MenuRoot and add the implement all the functions. Here we will add a menu called "Example" and create 2 sections: "Section 1" and "Section 2", with extension hook name "Section_1" and "Section_2".

+
+
+
ToolExampleEditor.cpp
+
+
TSharedRef<FWorkspaceItem> FToolExampleEditor::MenuRoot = FWorkspaceItem::NewGroup(FText::FromString("Menu Root"));
+
+
+void FToolExampleEditor::AddMenuExtension(const FMenuExtensionDelegate &extensionDelegate, FName extensionHook, const TSharedPtr<FUICommandList> &CommandList, EExtensionHook::Position position)
+{
+    MenuExtender->AddMenuExtension(extensionHook, position, CommandList, extensionDelegate);
+}
+
+void FToolExampleEditor::MakePulldownMenu(FMenuBarBuilder &menuBuilder)
+{
+    menuBuilder.AddPullDownMenu(
+        FText::FromString("Example"),
+        FText::FromString("Open the Example menu"),
+        FNewMenuDelegate::CreateRaw(this, &FToolExampleEditor::FillPulldownMenu),
+        "Example",
+        FName(TEXT("ExampleMenu"))
+    );
+}
+
+void FToolExampleEditor::FillPulldownMenu(FMenuBuilder &menuBuilder)
+{
+    // just a frame for tools to fill in
+    menuBuilder.BeginSection("ExampleSection", FText::FromString("Section 1"));
+    menuBuilder.AddMenuSeparator(FName("Section_1"));
+    menuBuilder.EndSection();
+
+    menuBuilder.BeginSection("ExampleSection", FText::FromString("Section 2"));
+    menuBuilder.AddMenuSeparator(FName("Section_2"));
+    menuBuilder.EndSection();
+}
+
+
+
+

Finally in StartupModule we add the following before we call the parent function. We add our menu after "Window" menu.

+
+
+
ToolExampleEditor.cpp
+
+
void FToolExampleEditor::StartupModule()
+{
+    if (!IsRunningCommandlet())
+    {
+        FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
+        LevelEditorMenuExtensibilityManager = LevelEditorModule.GetMenuExtensibilityManager();
+        MenuExtender = MakeShareable(new FExtender);
+        MenuExtender->AddMenuBarExtension("Window", EExtensionHook::After, NULL, FMenuBarExtensionDelegate::CreateRaw(this, &FToolExampleEditor::MakePulldownMenu));
+        LevelEditorMenuExtensibilityManager->AddExtender(MenuExtender);
+    }
+    IExampleModuleInterface::StartupModule();
+}
+
+
+
+

Now if you run it you should see the custom menu get added with two sections.

+
+
+
+002.png +
+
+
+

Next we can add our first tool to register to our menu. First add two new files:

+
+
+
+003.png +
+
+
+

This class will inherit from IExampleModuleListenerInterface, and we add function to create menu entry. We also add FUICommandList, which will define and map a menu item to a function. Finally we add our only menu function MenuCommand1, this function will be called when user click on the menu item.

+
+
+
MenuTool.h
+
+
#include "ToolExampleEditor/IExampleModuleInterface.h"
+
+class MenuTool : public IExampleModuleListenerInterface, public TSharedFromThis<MenuTool>
+{
+public:
+    virtual ~MenuTool() {}
+
+    virtual void OnStartupModule() override;
+    virtual void OnShutdownModule() override;
+
+    void MakeMenuEntry(FMenuBuilder &menuBuilder);
+
+protected:
+    TSharedPtr<FUICommandList> CommandList;
+
+    void MapCommands();
+
+    // UI Command functions
+    void MenuCommand1();
+};
+
+
+
+

On the cpp side, we got a lot more to do. First we need to define LOCTEXT_NAMESPACE at the beginning, and un-define it at the end. This is required to use UI_COMMAND macro. +Then we start filling in each command, first create a FUICommandInfo member for each command in command list class, fill in RegisterCommands function by using UI_COMMAND marcro. Then in MapCommands function map each command info to a function. And of course define the command function MenuTool::MenuCommand1.

+
+
+

In OnStartupModule, we create command list, register it, map it, then register to menu extension. In this case we want our item in "Section 1", and MakeMenuEntry will be called when Unreal build the menu, in which we simply add MenuCommand1 to the menu.

+
+
+

In OnShutdownModule, we need to unregister command list.

+
+
+
MenuTool.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "MenuTool.h"
+
+#define LOCTEXT_NAMESPACE "MenuTool"
+
+class MenuToolCommands : public TCommands<MenuToolCommands>
+{
+public:
+
+    MenuToolCommands()
+        : TCommands<MenuToolCommands>(
+        TEXT("MenuTool"), // Context name for fast lookup
+        FText::FromString("Example Menu tool"), // Context name for displaying
+        NAME_None,   // No parent context
+        FEditorStyle::GetStyleSetName() // Icon Style Set
+        )
+    {
+    }
+
+    virtual void RegisterCommands() override
+    {
+        UI_COMMAND(MenuCommand1, "Menu Command 1", "Test Menu Command 1.", EUserInterfaceActionType::Button, FInputGesture());
+
+    }
+
+public:
+    TSharedPtr<FUICommandInfo> MenuCommand1;
+};
+
+void MenuTool::MapCommands()
+{
+    const auto& Commands = MenuToolCommands::Get();
+
+    CommandList->MapAction(
+        Commands.MenuCommand1,
+        FExecuteAction::CreateSP(this, &MenuTool::MenuCommand1),
+        FCanExecuteAction());
+}
+
+void MenuTool::OnStartupModule()
+{
+    CommandList = MakeShareable(new FUICommandList);
+    MenuToolCommands::Register();
+    MapAction();
+    FToolExampleEditor::Get().AddMenuExtension(
+        FMenuExtensionDelegate::CreateRaw(this, &MenuTool::MakeMenuEntry),
+        FName("Section_1"),
+        CommandList);
+}
+
+void MenuTool::OnShutdownModule()
+{
+    MenuToolCommands::Unregister();
+}
+
+void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder)
+{
+    menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand1);
+}
+
+void MenuTool::MenuCommand1()
+{
+    UE_LOG(LogClass, Log, TEXT("clicked MenuCommand1"));
+}
+
+#undef LOCTEXT_NAMESPACE
+
+
+
+

When this is all done, remember to add this tool as a listener to editor module in FToolExampleEditor::AddModuleListeners:

+
+
+
ToolExampleEditor.cpp
+
+
ModuleListeners.Add(MakeShareable(new MenuTool));
+
+
+
+

Now if you build the project, you should see your menu item in the menu. And if you click on it, it will print "clicked MenuCommand1".

+
+
+

By now you have a basic framework for tools, You can run anything you want based on a menu click.

+
+
+
+004.png +
+
+
+
+
+

Advanced Menu

+
+
+

Before we jump to window, let’s extend menu functionality for a bit, since there are a lot more you can do.

+
+
+

First if you have a lot of items, it will be good to put them in a sub menu. Let’s make two more commands MenuCommand2 and MenuCommand3. You can search for MenuCommand1 and create two more in each places, other than MakeMenuEntry, where we will add sub menu.

+
+
+

In MenuTool, we add function for sub menu:

+
+
+
MenuTool.h
+
+
void MakeSubMenu(FMenuBuilder &menuBuilder);
+
+
+
+
MenuTool.cpp
+
+
void MenuTool::MakeSubMenu(FMenuBuilder &menuBuilder)
+{
+    menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand2);
+    menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand3);
+}
+
+
+
+

Then we call AddSubMenu in MenuTool::MakeMenuEntry, after MenuCommand1 is registered so the submenu comes after that.

+
+
+
MenuTool.cpp
+
+
void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder)
+{
+    ...
+    menuBuilder.AddSubMenu(
+        FText::FromString("Sub Menu"),
+        FText::FromString("This is example sub menu"),
+        FNewMenuDelegate::CreateSP(this, &MenuTool::MakeSubMenu)
+    );
+}
+
+
+
+

Now you should see sub menu like the following:

+
+
+
+005.png +
+
+
+

Not only you can add simple menu item, you can actually add any widget into the menu. We will try to make a small tool that you can type in a textbox and click a button to set that as tags for selected actors.

+
+
+

I’m not going to go into details for each functions I used here, search them in Unreal engine and you should find plenty of use cases.

+
+
+

First we add needed member and functions, note this time we are going to use custom widget, so we don’t need to change command list. For AddTag fucntion, because it is going to be used for a button, return type have to be FReply.

+
+
+
MenuTool.h
+
+
FName TagToAdd;
+
+FReply AddTag();
+FText GetTagToAddText() const;
+void OnTagToAddTextCommited(const FText& InText, ETextCommit::Type CommitInfo);
+
+
+
+

Then we fill in those functions. If you type in a text, we save it to TagToAdd. If you click on the button, we search all selected actors and make the tag change. We wrap it around a transaction so it will support undo. To use transaction we need to include "ScopedTransaction.h".

+
+
+
MenuTool.cpp
+
+
FReply MenuTool::AddTag()
+{
+    if (!TagToAdd.IsNone())
+    {
+        const FScopedTransaction Transaction(FText::FromString("Add Tag"));
+        for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It)
+        {
+            AActor* Actor = static_cast<AActor*>(*It);
+            if (!Actor->Tags.Contains(TagToAdd))
+            {
+                Actor->Modify();
+                Actor->Tags.Add(TagToAdd);
+            }
+        }
+    }
+    return FReply::Handled();
+}
+
+FText MenuTool::GetTagToAddText() const
+{
+    return FText::FromName(TagToAdd);
+}
+
+void MenuTool::OnTagToAddTextCommited(const FText& InText, ETextCommit::Type CommitInfo)
+{
+    FString str = InText.ToString();
+    TagToAdd = FName(*str.Trim());
+}
+
+
+
+

Then in MenuTool::MakeMenuEntry, we create the widget and add it to the menu. Again I will not go into Slate code details.

+
+
+
MenuTool.cpp
+
+
void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder)
+{
+    ...
+    TSharedRef<SWidget> AddTagWidget =
+        SNew(SHorizontalBox)
+        + SHorizontalBox::Slot()
+        .AutoWidth()
+        .VAlign(VAlign_Center)
+        [
+            SNew(SEditableTextBox)
+            .MinDesiredWidth(50)
+            .Text(this, &MenuTool::GetTagToAddText)
+            .OnTextCommitted(this, &MenuTool::OnTagToAddTextCommited)
+        ]
+        + SHorizontalBox::Slot()
+        .AutoWidth()
+        .Padding(5, 0, 0, 0)
+        .VAlign(VAlign_Center)
+        [
+            SNew(SButton)
+            .Text(FText::FromString("Add Tag"))
+            .OnClicked(this, &MenuTool::AddTag)
+        ];
+
+    menuBuilder.AddWidget(AddTagWidget, FText::FromString(""));
+}
+
+
+
+

Now you have a more complex tool sit in the menu, and you can set actor tags with it:

+
+
+
+006.png +
+
+
+
+
+

Create a Tab (Window)

+
+
+

While we can do a lot in the menu, it is still more convenient and flexible if you have a window. In Unreal it is called "tab". Because creating a tab from menu is very common for tools, we will make a base class for it first.

+
+
+

Add a new file:

+
+
+
+007.png +
+
+
+

The base class is also inherit from IExampleModuleListenerInterface. In OnStartupModule we register a tab, and unregister it in OnShutdownModule. Then in MakeMenuEntry, we let FGlobalTabmanager to populate tab for this menu item. +We leave SpawnTab function to be overriden by child class to set proper widget.

+
+
+
ExampleTabToolBase.h
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ToolExampleEditor/IExampleModuleInterface.h"
+#include "TabManager.h"
+#include "SDockTab.h"
+
+class FExampleTabToolBase : public IExampleModuleListenerInterface, public TSharedFromThis< FExampleTabToolBase >
+{
+public:
+    // IPixelopusToolBase
+    virtual void OnStartupModule() override
+    {
+        Initialize();
+        FGlobalTabmanager::Get()->RegisterNomadTabSpawner(TabName, FOnSpawnTab::CreateRaw(this, &FExampleTabToolBase::SpawnTab))
+            .SetGroup(FToolExampleEditor::Get().GetMenuRoot())
+            .SetDisplayName(TabDisplayName)
+            .SetTooltipText(ToolTipText);
+    };
+
+    virtual void OnShutdownModule() override
+    {
+        FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(TabName);
+    };
+
+    // In this function set TabName/TabDisplayName/ToolTipText
+    virtual void Initialize() {};
+    virtual TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs& TabSpawnArgs) { return SNew(SDockTab); };
+
+    virtual void MakeMenuEntry(FMenuBuilder &menuBuilder)
+    {
+        FGlobalTabmanager::Get()->PopulateTabSpawnerMenu(menuBuilder, TabName);
+    };
+
+protected:
+    FName TabName;
+    FText TabDisplayName;
+    FText ToolTipText;
+};
+
+
+
+

Now we add files for tab tool. Other than the normal tool class, we also need a custom panel widget class for the tab itself.

+
+
+
+008.png +
+
+
+

Let’s look at TabTool class first, it is inherited from ExampleTabToolBase defined above.

+
+
+

We set tab name, display name and tool tips in Initialize function, and prepare the panel in SpawnTab function. Note here we send the tool object itself as a parameter when creating the panel. This is not necessary, but as an example how you can pass in an object to the widget.

+
+
+

This tab tool is added in "Section 2" in the custom menu.

+
+
+
TabTool.h
+
+
#include "ToolExampleEditor/ExampleTabToolBase.h"
+
+class TabTool : public FExampleTabToolBase
+{
+public:
+    virtual ~TabTool () {}
+    virtual void OnStartupModule() override;
+    virtual void OnShutdownModule() override;
+    virtual void Initialize() override;
+    virtual TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs& TabSpawnArgs) override;
+};
+
+
+
+
TabTool.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "TabToolPanel.h"
+#include "TabTool.h"
+
+void TabTool::OnStartupModule()
+{
+    FExampleTabToolBase::OnStartupModule();
+    FToolExampleEditor::Get().AddMenuExtension(FMenuExtensionDelegate::CreateRaw(this, &TabTool::MakeMenuEntry), FName("Section_2"));
+}
+
+void TabTool::OnShutdownModule()
+{
+    FExampleTabToolBase::OnShutdownModule();
+}
+
+void TabTool::Initialize()
+{
+    TabName = "TabTool";
+    TabDisplayName = FText::FromString("Tab Tool");
+    ToolTipText = FText::FromString("Tab Tool Window");
+}
+
+TSharedRef<SDockTab> TabTool::SpawnTab(const FSpawnTabArgs& TabSpawnArgs)
+{
+    TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
+        .TabRole(ETabRole::NomadTab)
+        [
+            SNew(TabToolPanel)
+            .Tool(SharedThis(this))
+        ];
+
+    return SpawnedTab;
+}
+
+
+
+

Now for the pannel:

+
+
+

In the construct function we build the slate widget in ChildSlot. Here I’m add a scroll box, with a grey border inside, with a text box inside.

+
+
+
TabToolPanel.h
+
+
#include "SDockTab.h"
+#include "SDockableTab.h"
+#include "SDockTabStack.h"
+#include "SlateApplication.h"
+#include "TabTool.h"
+
+class TabToolPanel : public SCompoundWidget
+{
+    SLATE_BEGIN_ARGS(TabToolPanel)
+    {}
+    SLATE_ARGUMENT(TWeakPtr<class TabTool>, Tool)
+    SLATE_END_ARGS()
+
+    void Construct(const FArguments& InArgs);
+
+protected:
+    TWeakPtr<TabTool> tool;
+};
+
+
+
+
TabToolPanel.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "TabToolPanel.h"
+
+void TabToolPanel::Construct(const FArguments& InArgs)
+{
+    tool = InArgs._Tool;
+    if (tool.IsValid())
+    {
+        // do anything you need from tool object
+    }
+
+    ChildSlot
+    [
+        SNew(SScrollBox)
+        + SScrollBox::Slot()
+        .VAlign(VAlign_Top)
+        .Padding(5)
+        [
+            SNew(SBorder)
+            .BorderBackgroundColor(FColor(192, 192, 192, 255))
+            .Padding(15.0f)
+            [
+                SNew(STextBlock)
+                .Text(FText::FromString(TEXT("This is a tab example.")))
+            ]
+        ]
+    ];
+}
+
+
+
+

Finally remember to add this tool to editor module in FToolExampleEditor::AddModuleListeners:

+
+
+
ToolExampleEditor.cpp
+
+
ModuleListeners.Add(MakeShareable(new TabTool));
+
+
+
+

Now you can see tab tool in our custom menu:

+
+
+
+009.png +
+
+
+

When you click on it, it will populate a window you can dock anywhere as regular Unreal tab.

+
+
+
+010.png +
+
+
+
+
+

Customize Details Panel

+
+
+

Another commonly used feature is to customize the details panel for any UObject.

+
+
+

To show how it works, we will create an Actor class first in our game module "ToolExample". Add the follow file:

+
+
+
+011.png +
+
+
+

In this class, we add 2 booleans in "Options" category, and an integer in "Test" category. Remember to add "TOOLEXAMPLE_API" in front of class name to export it from game module, otherwise we cannot use it in editor module.

+
+
+
ExampleActor.h
+
+
#pragma once
+#include "ExampleActor.generated.h"
+
+UCLASS()
+class TOOLEXAMPLE_API AExampleActor : public AActor
+{
+    GENERATED_BODY()
+public:
+    UPROPERTY(EditAnywhere, Category = "Options")
+    bool bOption1 = false;
+
+    UPROPERTY(EditAnywhere, Category = "Options")
+    bool bOption2 = false;
+
+    UPROPERTY(EditAnywhere, Category = "Test")
+    int testInt = 0;
+};
+
+
+
+

Now if we load up Unreal and drag a "ExampleActor", you should see the following in the details panel:

+
+
+
+012.png +
+
+
+

If we want option 1 and option 2 to be mutually exclusive. You can have both unchecked or one of them checked, but you cannot have both checked. We want to customize this details panel, so if user check one of them, it will automatically uncheck the other.

+
+
+

Add the following files to editor module "ToolExampleEditor":

+
+
+
+013.png +
+
+
+

The details customization implements IDetailCustomization interface. In the main entry point CustomizeDetails function, we first hide original properties option 1 and option 2 (you can comment out those two lines and see how it works). Then we add our custom widget, here the "RadioButton" is purely a visual style, it has nothing to do with mutually exclusive logic. You can implement the same logic with other visuals like regular check box, buttons, etc.

+
+
+

In the widget functions for check box, IsModeRadioChecked and OnModeRadioChanged we add extra parameters "actor" and "optionIndex", so we can pass in the editing object and specify option when we construct the widget.

+
+
+
ExampleActorDetails.h
+
+
#pragma once
+#include "IDetailCustomization.h"
+
+class AExampleActor;
+
+class FExampleActorDetails : public IDetailCustomization
+{
+public:
+    /** Makes a new instance of this detail layout class for a specific detail view requesting it */
+    static TSharedRef<IDetailCustomization> MakeInstance();
+
+    /** IDetailCustomization interface */
+    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override;
+
+protected:
+    // widget functions
+    ECheckBoxState IsModeRadioChecked(AExampleActor* actor, int optionIndex) const;
+    void OnModeRadioChanged(ECheckBoxState CheckType, AExampleActor* actor, int optionIndex);
+};
+
+
+
+
ExampleActorDetails.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleActorDetails.h"
+#include "DetailsCustomization/ExampleActor.h"
+
+TSharedRef<IDetailCustomization> FExampleActorDetails::MakeInstance()
+{
+    return MakeShareable(new FExampleActorDetails);
+}
+
+void FExampleActorDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
+{
+    TArray<TWeakObjectPtr<UObject>> Objects;
+    DetailLayout.GetObjectsBeingCustomized(Objects);
+    if (Objects.Num() != 1)
+    {
+        // skip customization if select more than one objects
+        return;
+    }
+    AExampleActor* actor = (AExampleActor*)Objects[0].Get();
+
+    // hide original property
+    DetailLayout.HideProperty(DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(AExampleActor, bOption1)));
+    DetailLayout.HideProperty(DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(AExampleActor, bOption2)));
+
+    // add custom widget to "Options" category
+    IDetailCategoryBuilder& OptionsCategory = DetailLayout.EditCategory("Options", FText::FromString(""), ECategoryPriority::Important);
+    OptionsCategory.AddCustomRow(FText::FromString("Options"))
+                .WholeRowContent()
+                [
+                    SNew(SHorizontalBox)
+                    + SHorizontalBox::Slot()
+                    .AutoWidth()
+                    .VAlign(VAlign_Center)
+                    [
+                        SNew(SCheckBox)
+                        .Style(FEditorStyle::Get(), "RadioButton")
+                        .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 1)
+                        .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 1)
+                        [
+                            SNew(STextBlock).Text(FText::FromString("Option 1"))
+                        ]
+                    ]
+                    + SHorizontalBox::Slot()
+                    .AutoWidth()
+                    .Padding(10.f, 0.f, 0.f, 0.f)
+                    .VAlign(VAlign_Center)
+                    [
+                        SNew(SCheckBox)
+                        .Style(FEditorStyle::Get(), "RadioButton")
+                        .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 2)
+                        .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 2)
+                        [
+                            SNew(STextBlock).Text(FText::FromString("Option 2"))
+                        ]
+                    ]
+                ];
+}
+
+ECheckBoxState FExampleActorDetails::IsModeRadioChecked(AExampleActor* actor, int optionIndex) const
+{
+    bool bFlag = false;
+    if (actor)
+    {
+        if (optionIndex == 1)
+            bFlag = actor->bOption1;
+        else if (optionIndex == 2)
+            bFlag = actor->bOption2;
+    }
+    return bFlag ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
+}
+
+void FExampleActorDetails::OnModeRadioChanged(ECheckBoxState CheckType, AExampleActor* actor, int optionIndex)
+{
+    bool bFlag = (CheckType == ECheckBoxState::Checked);
+    if (actor)
+    {
+        actor->Modify();
+        if (bFlag)
+        {
+            // clear all options first
+            actor->bOption1 = false;
+            actor->bOption2 = false;
+        }
+        if (optionIndex == 1)
+            actor->bOption1 = bFlag;
+        else if (optionIndex == 2)
+            actor->bOption2 = bFlag;
+    }
+}
+
+
+
+

Then we need to register the layout in FToolExampleEditor::StartupModule and unregister it in FToolExampleEditor::ShutdownModule

+
+
+
ToolExampleEditor.cpp
+
+
#include "DetailsCustomization/ExampleActor.h"
+#include "DetailsCustomization/ExampleActorDetails.h"
+
+void FToolExampleEditor::StartupModule()
+{
+    ...
+
+    // register custom layouts
+    {
+        static FName PropertyEditor("PropertyEditor");
+        FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(PropertyEditor);
+        PropertyModule.RegisterCustomClassLayout(AExampleActor::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FExampleActorDetails::MakeInstance));
+    }
+
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    // unregister custom layouts
+    if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
+    {
+        FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
+        PropertyModule.UnregisterCustomClassLayout(AExampleActor::StaticClass()->GetFName());
+    }
+
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+

Now you should see the customized details panel:

+
+
+
+014.png +
+
+
+
+
+

Custom Data Type

+
+
+

New Custom Data

+
+

For simple data, you can just inherit from UDataAsset class, then you can create your data object in Unreal content browser: Add New → miscellaneous → Data Asset

+
+
+

If you want to add you data to a custom category, you need to do a bit more work.

+
+
+

First we need to create a custom data type in game module (ExampleTool). We will make one with only one property.

+
+
+
+015.png +
+
+
+

We add "SourceFilePath" for future sections.

+
+
+
ExampleData.h
+
+
#pragma once
+#include "ExampleData.generated.h"
+
+UCLASS(Blueprintable)
+class UExampleData : public UObject
+{
+    GENERATED_BODY()
+
+public:
+    UPROPERTY(EditAnywhere, Category = "Properties")
+    FString ExampleString;
+
+#if WITH_EDITORONLY_DATA
+    UPROPERTY(Category = SourceAsset, VisibleAnywhere)
+    FString SourceFilePath;
+#endif
+};
+
+
+
+

Then in editor module, add the following files:

+
+
+
+016.png +
+
+
+

We first make the factory:

+
+
+
ExampleDataFactory.h
+
+
#pragma once
+#include "UnrealEd.h"
+#include "ExampleDataFactory.generated.h"
+
+UCLASS()
+class UExampleDataFactory : public UFactory
+{
+    GENERATED_UCLASS_BODY()
+public:
+    virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
+};
+
+
+
+
ExampleDataFactory.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleDataFactory.h"
+#include "CustomDataType/ExampleData.h"
+
+UExampleDataFactory::UExampleDataFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
+{
+    SupportedClass = UExampleData::StaticClass();
+    bCreateNew = true;
+    bEditAfterNew = true;
+}
+
+UObject* UExampleDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
+{
+    UExampleData* NewObjectAsset = NewObject<UExampleData>(InParent, Class, Name, Flags | RF_Transactional);
+    return NewObjectAsset;
+}
+
+
+
+

Then we make type actions, here we will pass in the asset category.

+
+
+
ExampleDataTypeActions.h
+
+
#pragma once
+#include "AssetTypeActions_Base.h"
+
+class FExampleDataTypeActions : public FAssetTypeActions_Base
+{
+public:
+    FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory);
+
+    // IAssetTypeActions interface
+    virtual FText GetName() const override;
+    virtual FColor GetTypeColor() const override;
+    virtual UClass* GetSupportedClass() const override;
+    virtual uint32 GetCategories() override;
+    // End of IAssetTypeActions interface
+
+private:
+    EAssetTypeCategories::Type MyAssetCategory;
+};
+
+
+
+
ExampleDataTypeActions.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleDataTypeActions.h"
+#include "CustomDataType/ExampleData.h"
+
+FExampleDataTypeActions::FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory)
+    : MyAssetCategory(InAssetCategory)
+{
+}
+
+FText FExampleDataTypeActions::GetName() const
+{
+    return FText::FromString("Example Data");
+}
+
+FColor FExampleDataTypeActions::GetTypeColor() const
+{
+    return FColor(230, 205, 165);
+}
+
+UClass* FExampleDataTypeActions::GetSupportedClass() const
+{
+    return UExampleData::StaticClass();
+}
+
+uint32 FExampleDataTypeActions::GetCategories()
+{
+    return MyAssetCategory;
+}
+
+
+
+

Finally we need to register type actions in editor module. We add an array CreatedAssetTypeActions to save all type actions we registered, so we can unregister them properly when module is unloaded:

+
+
+
ToolExampleEditor.h
+
+
class FToolExampleEditor : public IExampleModuleInterface
+{
+    ...
+    TArray<TSharedPtr<IAssetTypeActions>> CreatedAssetTypeActions;
+}
+
+
+
+

In StartupModule function, we create a new "Example" category, and use that to register our type action.

+
+
+
ToolExampleEditor.cpp
+
+
#include "CustomDataType/ExampleDataTypeActions.h"
+
+void FToolExampleEditor::StartupModule()
+{
+    ...
+
+    // register custom types:
+    {
+        IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
+        // add custom category
+        EAssetTypeCategories::Type ExampleCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Example")), FText::FromString("Example"));
+        // register our custom asset with example category
+        TSharedPtr<IAssetTypeActions> Action = MakeShareable(new FExampleDataTypeActions(ExampleCategory));
+        AssetTools.RegisterAssetTypeActions(Action.ToSharedRef());
+        // saved it here for unregister later
+        CreatedAssetTypeActions.Add(Action);
+    }
+
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    ...
+
+    // Unregister all the asset types that we registered
+    if (FModuleManager::Get().IsModuleLoaded("AssetTools"))
+    {
+        IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
+        for (int32 i = 0; i < CreatedAssetTypeActions.Num(); ++i)
+        {
+            AssetTools.UnregisterAssetTypeActions(CreatedAssetTypeActions[i].ToSharedRef());
+        }
+    }
+    CreatedAssetTypeActions.Empty();
+
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+

Now you will see your data in proper category.

+
+
+
+017.png +
+
+
+
+

Import Custom Data

+
+

For all the hard work we did above, we can now our data from a file, like the way you can drag and drop an PNG file to create a texture. In this case we will have a text file, with extension ".xmp", to be imported into unreal, and we just set the text from the file to "ExampleString" property.

+
+
+

To make it work with import, we actually have to disable the ability to be able to create a new data from scratch. Modify factory class as following:

+
+
+
ExampleDataFactory.h
+
+
class UExampleDataFactory : public UFactory
+{
+    ...
+
+    virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn) override;
+    virtual bool FactoryCanImport(const FString& Filename) override;
+
+    // helper function
+    static void MakeExampleDataFromText(class UExampleData* Data, const TCHAR*& Buffer, const TCHAR* BufferEnd);
+};
+
+
+
+
ExampleDataFactory.cpp
+
+
UExampleDataFactory::UExampleDataFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
+{
+    Formats.Add(TEXT("xmp;Example Data"));
+    SupportedClass = UExampleData::StaticClass();
+    bCreateNew = false; // turned off for import
+    bEditAfterNew = false; // turned off for import
+    bEditorImport = true;
+    bText = true;
+}
+
+
+UObject* UExampleDataFactory::FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn)
+{
+    FEditorDelegates::OnAssetPreImport.Broadcast(this, InClass, InParent, InName, Type);
+
+    // if class type or extension doesn't match, return
+    if (InClass != UExampleData::StaticClass() ||
+        FCString::Stricmp(Type, TEXT("xmp")) != 0)
+        return nullptr;
+
+    UExampleData* Data = CastChecked<UExampleData>(NewObject<UExampleData>(InParent, InName, Flags));
+    MakeExampleDataFromText(Data, Buffer, BufferEnd);
+
+    // save the source file path
+    Data->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, Data->GetOutermost());
+
+    FEditorDelegates::OnAssetPostImport.Broadcast(this, Data);
+
+    return Data;
+}
+
+bool UExampleDataFactory::FactoryCanImport(const FString& Filename)
+{
+    return FPaths::GetExtension(Filename).Equals(TEXT("xmp"));
+}
+
+void UExampleDataFactory::MakeExampleDataFromText(class UExampleData* Data, const TCHAR*& Buffer, const TCHAR* BufferEnd)
+{
+    Data->ExampleString = Buffer;
+}
+
+
+
+

Note we changed bCreateNew and bEditAfterNew to false. We set "SourceFilePath" so we can do reimport later. If you want to import binary file, set bText = false, and override FactoryCreateBinary function instead.

+
+
+

Now you can drag & drop a xmp file and have the content imported automatically.

+
+
+
+018.png +
+
+
+

If you want to have custom editor for the data, you can follow "Customize Details Panel" section to create custom widget. Or you can override OpenAssetEditor function in ExampleDataTypeActions, to create a complete different editor. We are not going to dive in here, search "OpenAssetEditor" in Unreal engine for examples.

+
+
+
+

Reimport

+
+

To reimport a file, we need to implement a different factory class. The implementation should be straight forward.

+
+
+
+019.png +
+
+
+
ReimportExampleDataFactory.h
+
+
#pragma once
+#include "ExampleDataFactory.h"
+#include "ReimportExampleDataFactory.generated.h"
+
+UCLASS()
+class UReimportExampleDataFactory : public UExampleDataFactory, public FReimportHandler
+{
+    GENERATED_BODY()
+
+    // Begin FReimportHandler interface
+    virtual bool CanReimport(UObject* Obj, TArray<FString>& OutFilenames) override;
+    virtual void SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths) override;
+    virtual EReimportResult::Type Reimport(UObject* Obj) override;
+    // End FReimportHandler interface
+};
+
+
+
+
ReimportExampleDataFactory.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ReimportExampleDataFactory.h"
+#include "ExampleDataFactory.h"
+#include "CustomDataType/ExampleData.h"
+
+bool UReimportExampleDataFactory::CanReimport(UObject* Obj, TArray<FString>& OutFilenames)
+{
+    UExampleData* ExampleData = Cast<UExampleData>(Obj);
+    if (ExampleData)
+    {
+        OutFilenames.Add(UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost()));
+        return true;
+    }
+    return false;
+}
+
+void UReimportExampleDataFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
+{
+    UExampleData* ExampleData = Cast<UExampleData>(Obj);
+    if (ExampleData && ensure(NewReimportPaths.Num() == 1))
+    {
+        ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(NewReimportPaths[0], ExampleData->GetOutermost());
+    }
+}
+
+EReimportResult::Type UReimportExampleDataFactory::Reimport(UObject* Obj)
+{
+    UExampleData* ExampleData = Cast<UExampleData>(Obj);
+    if (!ExampleData)
+    {
+        return EReimportResult::Failed;
+    }
+
+    const FString Filename = UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost());
+    if (!FPaths::GetExtension(Filename).Equals(TEXT("xmp")))
+    {
+        return EReimportResult::Failed;
+    }
+
+    CurrentFilename = Filename;
+    FString Data;
+    if (FFileHelper::LoadFileToString(Data, *CurrentFilename))
+    {
+        const TCHAR* Ptr = *Data;
+        ExampleData->Modify();
+        ExampleData->MarkPackageDirty();
+
+        UExampleDataFactory::MakeExampleDataFromText(ExampleData, Ptr, Ptr + Data.Len());
+
+        // save the source file path and timestamp
+        ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, ExampleData->GetOutermost());
+    }
+
+    return EReimportResult::Succeeded;
+}
+
+
+
+

And just for fun, let’s add "Reimport" to right click menu on this asset. This is also an example for how to add more actions on specific asset type. Modify ExampleDataTypeActions class:

+
+
+
ExampleDataTypeActions.h
+
+
class FExampleDataTypeActions : public FAssetTypeActions_Base
+{
+public:
+    ...
+    virtual bool HasActions(const TArray<UObject*>& InObjects) const override { return true; }
+    virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;
+
+    void ExecuteReimport(TArray<TWeakObjectPtr<UExampleData>> Objects);
+};
+
+
+
+
ExampleDataTypeActions.cpp
+
+
void FExampleDataTypeActions::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
+{
+    auto ExampleDataImports = GetTypedWeakObjectPtrs<UExampleData>(InObjects);
+
+    MenuBuilder.AddMenuEntry(
+        FText::FromString("Reimport"),
+        FText::FromString("Reimports example data."),
+        FSlateIcon(),
+        FUIAction(
+            FExecuteAction::CreateSP(this, &FExampleDataTypeActions::ExecuteReimport, ExampleDataImports),
+            FCanExecuteAction()
+        )
+    );
+}
+
+void FExampleDataTypeActions::ExecuteReimport(TArray<TWeakObjectPtr<UExampleData>> Objects)
+{
+    for (auto ObjIt = Objects.CreateConstIterator(); ObjIt; ++ObjIt)
+    {
+        auto Object = (*ObjIt).Get();
+        if (Object)
+        {
+            FReimportManager::Instance()->Reimport(Object, /*bAskForNewFileIfMissing=*/true);
+        }
+    }
+}
+
+
+
+

Now you can reimport your custom files.

+
+
+
+020.png +
+
+
+
+
+
+

Custom Editor Mode

+
+
+

Editor Mode is probably the most powerful tool framework in Unreal. You will get and react to all user input; you can render to viewport; you can monitor any change in the scene and get Undo/Redo events. Remember you can enter a mode and paint foliage over objects? You can do the same degree of stuff in custom editor mode. Editor Mode has dedicated section in UI layout, and you can customize the widget here as well.

+
+
+
+021.png +
+
+
+

Here as an example, we will create an editor mode to do a simple task. We have an actor "ExampleTargetPoint" inherit from "TargetPoint", with a list of locations. In this editor mode we want to visualize those points. You can create new points or delete points. You can also move points around as moving normal objects. Note this is not the best way for this functionality (you can use MakeEditWidget in UPROPERTY to do this easily), but rather as a way to demonstrate how to set it up and what you can potentially do.

+
+
+

Setup Editor Mode

+
+

First we need to create an icon for our editor mode. We make an 40x40 PNG file as \Content\EditorResources\IconExampleEditorMode.png

+
+
+

Then add the following files in editor module:

+
+
+
+022.png +
+
+
+

SExampleEdModeWidget is the widget we use in "Modes" panel. Here we will just create a simple one for now. We also include a commonly used util function to get EdMode object.

+
+
+
SExampleEdModeWidget.h
+
+
#pragma once
+#include "SlateApplication.h"
+
+class SExampleEdModeWidget : public SCompoundWidget
+{
+public:
+    SLATE_BEGIN_ARGS(SExampleEdModeWidget) {}
+    SLATE_END_ARGS();
+
+    void Construct(const FArguments& InArgs);
+
+    // Util Functions
+    class FExampleEdMode* GetEdMode() const;
+};
+
+
+
+
SExampleEdModeWidget.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleEdMode.h"
+#include "SExampleEdModeWidget.h"
+
+void SExampleEdModeWidget::Construct(const FArguments& InArgs)
+{
+    ChildSlot
+    [
+        SNew(SScrollBox)
+        + SScrollBox::Slot()
+        .VAlign(VAlign_Top)
+        .Padding(5.f)
+        [
+            SNew(STextBlock)
+            .Text(FText::FromString(TEXT("This is a editor mode example.")))
+        ]
+    ];
+}
+
+FExampleEdMode* SExampleEdModeWidget::GetEdMode() const
+{
+    return (FExampleEdMode*)GLevelEditorModeTools().GetActiveMode(FExampleEdMode::EM_Example);
+}
+
+
+
+

ExampleEdModeToolkit is a middle layer between EdMode and its widget:

+
+
+
ExampleEdModeToolkit.h
+
+
#pragma once
+#include "BaseToolkit.h"
+#include "ExampleEdMode.h"
+#include "SExampleEdModeWidget.h"
+
+class FExampleEdModeToolkit: public FModeToolkit
+{
+public:
+    FExampleEdModeToolkit()
+    {
+        SAssignNew(ExampleEdModeWidget, SExampleEdModeWidget);
+    }
+
+    /** IToolkit interface */
+    virtual FName GetToolkitFName() const override { return FName("ExampleEdMode"); }
+    virtual FText GetBaseToolkitName() const override { return NSLOCTEXT("BuilderModeToolkit", "DisplayName", "Builder"); }
+    virtual class FEdMode* GetEditorMode() const override { return GLevelEditorModeTools().GetActiveMode(FExampleEdMode::EM_Example); }
+    virtual TSharedPtr<class SWidget> GetInlineContent() const override { return ExampleEdModeWidget; }
+
+private:
+    TSharedPtr<SExampleEdModeWidget> ExampleEdModeWidget;
+};
+
+
+
+

Then for the main class ExampleEdMode. Since we are only try to set it up, we will leave it mostly empty, only setting up its ID and create toolkit object. We will fill it in heavily in the next section.

+
+
+
ExampleEdMode.h
+
+
#pragma once
+#include "EditorModes.h"
+
+class FExampleEdMode : public FEdMode
+{
+public:
+    const static FEditorModeID EM_Example;
+
+    // FEdMode interface
+    virtual void Enter() override;
+    virtual void Exit() override;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "Editor/UnrealEd/Public/Toolkits/ToolkitManager.h"
+#include "ScopedTransaction.h"
+#include "ExampleEdModeToolkit.h"
+#include "ExampleEdMode.h"
+
+const FEditorModeID FExampleEdMode::EM_Example(TEXT("EM_Example"));
+
+void FExampleEdMode::Enter()
+{
+    FEdMode::Enter();
+
+    if (!Toolkit.IsValid())
+    {
+        Toolkit = MakeShareable(new FExampleEdModeToolkit);
+        Toolkit->Init(Owner->GetToolkitHost());
+    }
+}
+
+void FExampleEdMode::Exit()
+{
+    FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef());
+    Toolkit.Reset();
+
+    FEdMode::Exit();
+}
+
+
+
+

As other tools, we need a tool class to handle registration. Here we need to register both editor mode and its icon.

+
+
+
ExampleEdModeTool.h
+
+
#pragma once
+#include "ToolExampleEditor/ExampleTabToolBase.h"
+
+class ExampleEdModeTool : public FExampleTabToolBase
+{
+public:
+    virtual void OnStartupModule() override;
+    virtual void OnShutdownModule() override;
+
+    virtual ~ExampleEdModeTool() {}
+private:
+    static TSharedPtr< class FSlateStyleSet > StyleSet;
+
+    void RegisterStyleSet();
+    void UnregisterStyleSet();
+
+    void RegisterEditorMode();
+    void UnregisterEditorMode();
+};
+
+
+
+
ExampleEdModeTool.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleEdModeTool.h"
+#include "ExampleEdMode.h"
+
+#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__)
+
+TSharedPtr< FSlateStyleSet > ExampleEdModeTool::StyleSet = nullptr;
+
+void ExampleEdModeTool::OnStartupModule()
+{
+    RegisterStyleSet();
+    RegisterEditorMode();
+}
+
+void ExampleEdModeTool::OnShutdownModule()
+{
+    UnregisterStyleSet();
+    UnregisterEditorMode();
+}
+
+void ExampleEdModeTool::RegisterStyleSet()
+{
+    // Const icon sizes
+    const FVector2D Icon20x20(20.0f, 20.0f);
+    const FVector2D Icon40x40(40.0f, 40.0f);
+
+    // Only register once
+    if (StyleSet.IsValid())
+    {
+        return;
+    }
+
+    StyleSet = MakeShareable(new FSlateStyleSet("ExampleEdModeToolStyle"));
+    StyleSet->SetContentRoot(FPaths::GameDir() / TEXT("Content/EditorResources"));
+    StyleSet->SetCoreContentRoot(FPaths::GameDir() / TEXT("Content/EditorResources"));
+
+    // Spline editor
+    {
+        StyleSet->Set("ExampleEdMode", new IMAGE_BRUSH(TEXT("IconExampleEditorMode"), Icon40x40));
+        StyleSet->Set("ExampleEdMode.Small", new IMAGE_BRUSH(TEXT("IconExampleEditorMode"), Icon20x20));
+    }
+
+    FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
+}
+
+void ExampleEdModeTool::UnregisterStyleSet()
+{
+    if (StyleSet.IsValid())
+    {
+        FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get());
+        ensure(StyleSet.IsUnique());
+        StyleSet.Reset();
+    }
+}
+
+void ExampleEdModeTool::RegisterEditorMode()
+{
+    FEditorModeRegistry::Get().RegisterMode<FExampleEdMode>(
+        FExampleEdMode::EM_Example,
+        FText::FromString("Example Editor Mode"),
+        FSlateIcon(StyleSet->GetStyleSetName(), "ExampleEdMode", "ExampleEdMode.Small"),
+        true, 500
+        );
+}
+
+void ExampleEdModeTool::UnregisterEditorMode()
+{
+    FEditorModeRegistry::Get().UnregisterMode(FExampleEdMode::EM_Example);
+}
+
+#undef IMAGE_BRUSH
+
+
+
+

Finally as usual, we add the tool to editor module FToolExampleEditor::AddModuleListeners:

+
+
+
ToolExampleEditor.cpp
+
+
ModuleListeners.Add(MakeShareable(new ExampleEdModeTool));
+
+
+
+

Now you should see our custom editor mode show up in "Modes" panel.

+
+
+
+023.png +
+
+
+
+

Render and Click

+
+

With the basic framework ready, we can actually start implementing tool logic. First we make ExampleTargetPoint class in game module. This actor holds points data, and is what our tool will be operating on. Again remember to export the class with TOOLEXAMPLE_API.

+
+
+
+024.png +
+
+
+
ExampleTargetPoint.h
+
+
#pragma once
+#include "Engine/Targetpoint.h"
+#include "ExampleTargetPoint.generated.h"
+
+UCLASS()
+class TOOLEXAMPLE_API AExampleTargetPoint : public ATargetPoint
+{
+    GENERATED_BODY()
+
+public:
+    UPROPERTY(EditAnywhere, Category = "Points")
+    TArray<FVector> Points;
+};
+
+
+
+

Now we modify ExampleEdMode to add functions to add point, remove point, and select point. We also save our current selection in variable, here we use weak object pointer to handle the case if the actor is removed.

+
+
+

For adding point, we only allow that when you have exactly on ExampleTargetPoint actor selected in editor. For removing point, we simply remove the current selected point if there is any. If you select any point, we will deselect all actors and select the actor associated with that point.

+
+
+

Note that we put FScopedTransaction, and called Modify() function whenever we modify data we need to save. This will make sure undo/redo is properly handled.

+
+
+
ExampleEdMode.h
+
+
...
+class AExampleTargetPoint;
+
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    void AddPoint();
+    bool CanAddPoint() const;
+    void RemovePoint();
+    bool CanRemovePoint() const;
+    bool HasValidSelection() const;
+    void SelectPoint(AExampleTargetPoint* actor, int32 index);
+
+    TWeakObjectPtr<AExampleTargetPoint> currentSelectedTarget;
+    int32 currentSelectedIndex = -1;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
void FExampleEdMode::Enter()
+{
+    ...
+
+    // reset
+    currentSelectedTarget = nullptr;
+    currentSelectedIndex = -1;
+}
+
+AExampleTargetPoint* GetSelectedTargetPointActor()
+{
+    TArray<UObject*> selectedObjects;
+    GEditor->GetSelectedActors()->GetSelectedObjects(selectedObjects);
+    if (selectedObjects.Num() == 1)
+    {
+        return Cast<AExampleTargetPoint>(selectedObjects[0]);
+    }
+    return nullptr;
+}
+
+void FExampleEdMode::AddPoint()
+{
+    AExampleTargetPoint* actor = GetSelectedTargetPointActor();
+    if (actor)
+    {
+        const FScopedTransaction Transaction(FText::FromString("Add Point"));
+
+        // add new point, slightly in front of camera
+        FEditorViewportClient* client = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient();
+        FVector newPoint = client->GetViewLocation() + client->GetViewRotation().Vector() * 50.f;
+        actor->Modify();
+        actor->Points.Add(newPoint);
+        // auto select this new point
+        SelectPoint(actor, actor->Points.Num() - 1);
+    }
+}
+
+bool FExampleEdMode::CanAddPoint() const
+{
+    return GetSelectedTargetPointActor() != nullptr;
+}
+
+void FExampleEdMode::RemovePoint()
+{
+    if (HasValidSelection())
+    {
+        const FScopedTransaction Transaction(FText::FromString("Remove Point"));
+
+        currentSelectedTarget->Modify();
+        currentSelectedTarget->Points.RemoveAt(currentSelectedIndex);
+        // deselect the point
+        SelectPoint(nullptr, -1);
+    }
+}
+
+bool FExampleEdMode::CanRemovePoint() const
+{
+    return HasValidSelection();
+}
+
+bool FExampleEdMode::HasValidSelection() const
+{
+    return currentSelectedTarget.IsValid() && currentSelectedIndex >= 0 && currentSelectedIndex < currentSelectedTarget->Points.Num();
+}
+
+void FExampleEdMode::SelectPoint(AExampleTargetPoint* actor, int32 index)
+{
+    currentSelectedTarget = actor;
+    currentSelectedIndex = index;
+
+    // select this actor only
+    if (currentSelectedTarget.IsValid())
+    {
+        GEditor->SelectNone(true, true);
+        GEditor->SelectActor(currentSelectedTarget.Get(), true, true);
+    }
+}
+
+
+
+

Now we have functionality ready, we still need to hook it up with UI. Modify to SExampleEdModeWidget add "Add" and "Remove" button, and we will check "CanAddPoint" and "CanRemovePoint" to determine if the button should be enabled.

+
+
+
SExampleEdModeWidget.h
+
+
class SExampleEdModeWidget : public SCompoundWidget
+{
+public:
+    ...
+    FReply OnAddPoint();
+    bool CanAddPoint() const;
+    FReply OnRemovePoint();
+    bool CanRemovePoint() const;
+};
+
+
+
+
SExampleEdModeWidget.cpp
+
+
void SExampleEdModeWidget::Construct(const FArguments& InArgs)
+{
+    ChildSlot
+    [
+        SNew(SScrollBox)
+        + SScrollBox::Slot()
+        .VAlign(VAlign_Top)
+        .Padding(5.f)
+        [
+            SNew(SVerticalBox)
+            + SVerticalBox::Slot()
+            .AutoHeight()
+            .Padding(0.f, 5.f, 0.f, 0.f)
+            [
+                SNew(STextBlock)
+                .Text(FText::FromString(TEXT("This is a editor mode example.")))
+            ]
+            + SVerticalBox::Slot()
+            .AutoHeight()
+            .Padding(0.f, 5.f, 0.f, 0.f)
+            [
+                SNew(SHorizontalBox)
+                + SHorizontalBox::Slot()
+                .AutoWidth()
+                .Padding(2, 0, 0, 0)
+                .VAlign(VAlign_Center)
+                [
+                    SNew(SButton)
+                    .Text(FText::FromString("Add"))
+                    .OnClicked(this, &SExampleEdModeWidget::OnAddPoint)
+                    .IsEnabled(this, &SExampleEdModeWidget::CanAddPoint)
+                ]
+                + SHorizontalBox::Slot()
+                .AutoWidth()
+                .VAlign(VAlign_Center)
+                .Padding(0, 0, 2, 0)
+                [
+                    SNew(SButton)
+                    .Text(FText::FromString("Remove"))
+                    .OnClicked(this, &SExampleEdModeWidget::OnRemovePoint)
+                    .IsEnabled(this, &SExampleEdModeWidget::CanRemovePoint)
+                ]
+            ]
+        ]
+    ];
+}
+
+FReply SExampleEdModeWidget::OnAddPoint()
+{
+    GetEdMode()->AddPoint();
+    return FReply::Handled();
+}
+
+bool SExampleEdModeWidget::CanAddPoint() const
+{
+    return GetEdMode()->CanAddPoint();
+}
+
+FReply SExampleEdModeWidget::OnRemovePoint()
+{
+    GetEdMode()->RemovePoint();
+    return FReply::Handled();
+}
+
+bool SExampleEdModeWidget::CanRemovePoint() const
+{
+    return GetEdMode()->CanRemovePoint();
+}
+
+
+
+

Now if you launch the editor, you should be able to drag in an "Example Target Point", switch to our editor mode, select that target point and add new points from the editor mode UI. However it is not visualized in the viewport yet, and you cannot click and select point. We will work on that next.

+
+
+

To be able to click in editor and select something, we need to define a HitProxy struct. When we render the points, we render with this hit proxy along with some data attached to it. Then when we get the click event, we can retrieve those data back from the proxy and know what we clicked on.

+
+
+

Back to ExampleEdMode, we define HExamplePointProxy with a reference object (the ExampleTargetPoint actor) and the point index, and we add Render and HandleClick override function.

+
+
+
ExampleEdMode.h
+
+
struct HExamplePointProxy : public HHitProxy
+{
+    DECLARE_HIT_PROXY();
+
+    HExamplePointProxy(UObject* InRefObject, int32 InIndex)
+        : HHitProxy(HPP_UI), RefObject(InRefObject), Index(InIndex)
+    {}
+
+    UObject* RefObject;
+    int32 Index;
+};
+
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
+    virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) override;
+};
+
+
+
+

Then in cpp file, we use macro IMPLEMENT_HIT_PROXY to implement the proxy. In Render we simply loops through all ExampleTargetPoint actor and draw all the points (and a line to the actor itself), we choose a different color if this is the current selected point. We set hit proxy for each point before drawing and clears it immediately afterwards (this is important so the proxy doesn’t leak through to other draws). In HandleClick, we test hit proxy and select point if we have a valid hit. We don’t check mouse button here, so you can select with left/right/middle click.

+
+
+
ExampleEdMode.cpp
+
+
IMPLEMENT_HIT_PROXY(HExamplePointProxy, HHitProxy);
+...
+
+void FExampleEdMode::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
+{
+    const FColor normalColor(200, 200, 200);
+    const FColor selectedColor(255, 128, 0);
+
+    UWorld* World = GetWorld();
+    for (TActorIterator<AExampleTargetPoint> It(World); It; ++It)
+    {
+        AExampleTargetPoint* actor = (*It);
+        if (actor)
+        {
+            FVector actorLoc = actor->GetActorLocation();
+            for (int i = 0; i < actor->Points.Num(); ++i)
+            {
+                bool bSelected = (actor == currentSelectedTarget && i == currentSelectedIndex);
+                const FColor& color = bSelected ? selectedColor : normalColor;
+                // set hit proxy and draw
+                PDI->SetHitProxy(new HExamplePointProxy(actor, i));
+                PDI->DrawPoint(actor->Points[i], color, 15.f, SDPG_Foreground);
+                PDI->DrawLine(actor->Points[i], actorLoc, color, SDPG_Foreground);
+                PDI->SetHitProxy(NULL);
+            }
+        }
+    }
+
+    FEdMode::Render(View, Viewport, PDI);
+}
+
+bool FExampleEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click)
+{
+    bool isHandled = false;
+
+    if (HitProxy)
+    {
+        if (HitProxy->IsA(HExamplePointProxy::StaticGetType()))
+        {
+            isHandled = true;
+            HExamplePointProxy* examplePointProxy = (HExamplePointProxy*)HitProxy;
+            AExampleTargetPoint* actor = Cast<AExampleTargetPoint>(examplePointProxy->RefObject);
+            int32 index = examplePointProxy->Index;
+            if (actor && index >= 0 && index < actor->Points.Num())
+            {
+                SelectPoint(actor, index);
+            }
+        }
+    }
+
+    return isHandled;
+}
+
+
+
+

With all of these you can start adding/removing points in the editor:

+
+
+
+025.png +
+
+
+
+

Use Transform Widget

+
+

The next mission is to be able to move point around in editor like moving any other actor. Go back to ExampleEdMode, this time we need to add support for custom transform widget, and handle InputDelta event. In InputDelta function, we don’t use FScopedTransaction because undo/redo is already handled for this function. We still need to call Modify() though.

+
+
+
ExampleEdMode.h
+
+
...
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override;
+    virtual bool ShowModeWidgets() const override;
+    virtual bool ShouldDrawWidget() const override;
+    virtual bool UsesTransformWidget() const override;
+    virtual FVector GetWidgetLocation() const override;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
bool FExampleEdMode::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale)
+{
+    if (InViewportClient->GetCurrentWidgetAxis() == EAxisList::None)
+    {
+        return false;
+    }
+
+    if (HasValidSelection())
+    {
+        if (!InDrag.IsZero())
+        {
+            currentSelectedTarget->Modify();
+            currentSelectedTarget->Points[currentSelectedIndex] += InDrag;
+        }
+        return true;
+    }
+
+    return false;
+}
+
+bool FExampleEdMode::ShowModeWidgets() const
+{
+    return true;
+}
+
+bool FExampleEdMode::ShouldDrawWidget() const
+{
+    return true;
+}
+
+bool FExampleEdMode::UsesTransformWidget() const
+{
+    return true;
+}
+
+FVector FExampleEdMode::GetWidgetLocation() const
+{
+    if (HasValidSelection())
+    {
+        return currentSelectedTarget->Points[currentSelectedIndex];
+    }
+    return FEdMode::GetWidgetLocation();
+}
+
+
+
+

Now you should have a transform widget to move your points around:

+
+
+
+026.png +
+
+
+
+
virtual bool GetCustomDrawingCoordinateSystem(FMatrix& InMatrix, void* InData) override;
+virtual bool GetCustomInputCoordinateSystem(FMatrix& InMatrix, void* InData) override;
+
+
+
+
+

Key input support, right click menu, and others

+
+

Next we will add some other common features: when we have a point selected, we want to hit delete button and remove it. Also we want to have a menu generated when you right click on a point, showing the point index, and an option to delete it.

+
+
+

Remember in the "Menu Tool" tutorial, in order to make a menu, we would need a UI command list, here we will do the same thing. We also override InputKey function to handle input. Though we can simply call functions based on which key is pressed, since we have the same functionality in the menu, we will route the input through the UI command list instead. (when we define UI Commands, we pass in a key in FInputGesture)

+
+
+

Finally we will modify HandleClick function to generate context menu when we right click on a point.

+
+
+
ExampleEdMode.h
+
+
...
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    FExampleEdMode();
+    ~FExampleEdMode();
+
+    virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) override;
+
+    TSharedPtr<FUICommandList> ExampleEdModeActions;
+    void MapCommands();
+    TSharedPtr<SWidget> GenerateContextMenu(FEditorViewportClient* InViewportClient) const;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
class ExampleEditorCommands : public TCommands<ExampleEditorCommands>
+{
+public:
+    ExampleEditorCommands() : TCommands <ExampleEditorCommands>
+        (
+            "ExampleEditor",    // Context name for fast lookup
+            FText::FromString(TEXT("Example Editor")),  // context name for displaying
+            NAME_None,  // Parent
+            FEditorStyle::GetStyleSetName()
+            )
+    {
+    }
+
+#define LOCTEXT_NAMESPACE ""
+    virtual void RegisterCommands() override
+    {
+        UI_COMMAND(DeletePoint, "Delete Point", "Delete the currently selected point.", EUserInterfaceActionType::Button, FInputGesture(EKeys::Delete));
+    }
+#undef LOCTEXT_NAMESPACE
+
+public:
+    TSharedPtr<FUICommandInfo> DeletePoint;
+};
+
+
+FExampleEdMode::FExampleEdMode()
+{
+    ExampleEditorCommands::Register();
+    ExampleEdModeActions = MakeShareable(new FUICommandList);
+}
+
+FExampleEdMode::~FExampleEdMode()
+{
+    ExampleEditorCommands::Unregister();
+}
+
+void FExampleEdMode::MapCommands()
+{
+    const auto& Commands = ExampleEditorCommands::Get();
+
+    ExampleEdModeActions->MapAction(
+        Commands.DeletePoint,
+        FExecuteAction::CreateSP(this, &FExampleEdMode::RemovePoint),
+        FCanExecuteAction::CreateSP(this, &FExampleEdMode::CanRemovePoint));
+}
+
+bool FExampleEdMode::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event)
+{
+    bool isHandled = false;
+
+    if (!isHandled && Event == IE_Pressed)
+    {
+        isHandled = ExampleEdModeActions->ProcessCommandBindings(Key, FSlateApplication::Get().GetModifierKeys(), false);
+    }
+
+    return isHandled;
+}
+
+TSharedPtr<SWidget> FExampleEdMode::GenerateContextMenu(FEditorViewportClient* InViewportClient) const
+{
+    FMenuBuilder MenuBuilder(true, NULL);
+
+    MenuBuilder.PushCommandList(ExampleEdModeActions.ToSharedRef());
+    MenuBuilder.BeginSection("Example Section");
+    if (HasValidSelection())
+    {
+        // add label for point index
+        TSharedRef<SWidget> LabelWidget =
+            SNew(STextBlock)
+            .Text(FText::FromString(FString::FromInt(currentSelectedIndex)))
+            .ColorAndOpacity(FLinearColor::Green);
+        MenuBuilder.AddWidget(LabelWidget, FText::FromString(TEXT("Point Index: ")));
+        MenuBuilder.AddMenuSeparator();
+        // add delete point entry
+        MenuBuilder.AddMenuEntry(ExampleEditorCommands::Get().DeletePoint);
+    }
+    MenuBuilder.EndSection();
+    MenuBuilder.PopCommandList();
+
+    TSharedPtr<SWidget> MenuWidget = MenuBuilder.MakeWidget();
+    return MenuWidget;
+}
+
+
+bool FExampleEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click)
+{
+    ...
+
+    if (HitProxy && isHandled && Click.GetKey() == EKeys::RightMouseButton)
+    {
+        TSharedPtr<SWidget> MenuWidget = GenerateContextMenu(InViewportClient);
+        if (MenuWidget.IsValid())
+        {
+            FSlateApplication::Get().PushMenu(
+                Owner->GetToolkitHost()->GetParentWidget(),
+                FWidgetPath(),
+                MenuWidget.ToSharedRef(),
+                FSlateApplication::Get().GetCursorPos(),
+                FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu));
+        }
+    }
+
+    return isHandled;
+}
+
+
+
+

The following is the result:

+
+
+
+027.png +
+
+
+

There are other virtual functions from FEdMode that can be very helpful. I’ll list some of them here:

+
+
+
+
    virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override;
+    virtual bool CapturedMouseMove(FEditorViewportClient* InViewportClient, FViewport* InViewport, int32 InMouseX, int32 InMouseY) override;
+    virtual bool StartTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) override;
+    virtual bool EndTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) override;
+    virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) override;
+    virtual void PostUndo() override;
+    virtual void ActorsDuplicatedNotify(TArray<AActor*>& PreDuplicateSelection, TArray<AActor*>& PostDuplicateSelection, bool bOffsetLocations) override;
+    virtual void ActorMoveNotify() override;
+    virtual void ActorSelectionChangeNotify() override;
+    virtual void MapChangeNotify() override;
+    virtual void SelectionChanged() override;
+
+
+
+
+
+
+

Custom Project Settings

+
+
+

Remember you can you go to Edit → Project Settings in Unreal editor to change various game/editor settings? You can add your custom settings to this window as well.

+
+
+

First we create a settings object. In this example we will create it in editor module, you can create in game module as well, just remember to export it with proper macro. +In the UCLASS macro, we need specify which .ini file to write to. You can use existing .ini file like "Game" or "Editor". In this case we want this setting to be per user and not shared on source control, so we create a new ini file. +For each UPROPERTY that you want to include in the settings, mark it with "config".

+
+
+
+028.png +
+
+
+
ExampleSettings.h
+
+
#pragma once
+#include "ExampleSettings.generated.h"
+
+UCLASS(config = EditorUserSettings, defaultconfig)
+class UExampleSettings : public UObject
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, config, Category = Test)
+    bool bTest = false;
+};
+
+
+
+
ToolExampleEditor.cpp
+
+
...
+#include "ISettingsModule.h"
+#include "Developer/Settings/Public/ISettingsContainer.h"
+#include "CustomProjectSettings/ExampleSettings.h"
+
+void FToolExampleEditor::StartupModule()
+{
+    ...
+    // register settings:
+    {
+        ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
+        if (SettingsModule)
+        {
+            TSharedPtr<ISettingsContainer> ProjectSettingsContainer = SettingsModule->GetContainer("Project");
+            ProjectSettingsContainer->DescribeCategory("ExampleCategory", FText::FromString("Example Category"), FText::FromString("Example settings description text here"));
+
+            SettingsModule->RegisterSettings("Project", "ExampleCategory", "ExampleSettings",
+                FText::FromString("Example Settings"),
+                FText::FromString("Configure Example Settings"),
+                GetMutableDefault<UExampleSettings>()
+            );
+        }
+    }
+
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    ...
+    // unregister settings
+    ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
+    if (SettingsModule)
+    {
+        SettingsModule->UnregisterSettings("Project", "ExampleCategory", "ExampleSettings");
+    }
+
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+

Now you should see your custom settings in "Project Settings" window. And when you change it, you should see DefaultEditorUserSettings.ini created in \ToolExample\Config

+
+
+
+029.png +
+
+
+

To get access to this settings, do the following:

+
+
+
+
const UExampleSettings* ExampleSettings = GetDefault<UExampleSettings>();
+if(ExampleSettings && ExampleSettings->bTest)
+    // do something
+
+
+
+
+
+

Tricks

+
+
+

Use Widget Reflector

+
+

The best way to learn SLATE and Unreal tools, is to use Widget Reflector. In Window → Developer Tool → Widget Reflector to launch the reflector. Click on "Pick Live Widget" and mouse over the widget you want to see, then hit "ESC" to freeze.

+
+
+

For example we can mouse over our editor mode widget, and you can see the structure showing in the reflector window. You can click on the file and it will take you to the exact place that widget is constructed. This is powerful tool to debug your widget or to learn how Unreal build their widget.

+
+
+
+030.png +
+
+
+
+

Is my tool running in the editor or game?

+
+

There 3 conditions that your tool is running:

+
+
+
    +
  1. +

    Editor: game not started, you can do all normal editing.

    +
  2. +
  3. +

    Game: game started, cannot do any editing.

    +
  4. +
  5. +

    Simulate: either hit “Simulate” or hit “Play” then “Eject”, game started and you can do limited editing. +Here is how you can determine which state you are in:

    +
  6. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + +

Editor

Game

Simulate

FApp::IsGame()

false

true

true

Cast<UEditorEngine>(GEngine)→bIsSimulatingInEditor

false

false

true

+
+

Note: this do NOT work in SLATE call (any UI tick for example), because that is in SLATE world.

+
+
+
+

Useful UPROPERTY() meta marker

+
+
    +
  • +

    MakeEditWidget: If you just need to visualize a point in the level and be able to drag it around, this is the quick way to do it. It works for FVector or FTransform, and it works with TArray of those as well.
    +example: UPROPERTY(meta = (MakeEditWidget = true))

    +
  • +
  • +

    DisplayName, ToolTip: Useful if you want to have a different display name than the variable name; or if you want add a mouse over tooltip. There are plenty of examples in Unreal code base.

    +
  • +
  • +

    ClampMin, ClampMax, UIMin, UIMax: You can specify a range for the value that can be input for this field.
    +example: UPROPERTY(meta = (ClampMin = "0", ClampMax = "180"))

    +
  • +
  • +

    EditCondition: You can specify a bool to determine whether this field is editable.
    +example: UPROPERTY(meta = (EditCondition = "bIsThisFieldEnabled")))

    +
  • +
+
+
+

For a complete list, search for ObjectMacros.h in Unreal code base.

+
+
+
+

Make custom Animation Blueprint Node

+
+

To make a custom Animation Blueprint Node, you need to first inherit from FAnimNode_Base class in game module, this class will process animation pose at runtime.

+
+
+

Then in the editor module, inherit from UAnimGraphNode_Base class, and define how you want this node to be in editor.

+
+
+
+

Debug Draw Tricks

+
+
    +
  • +

    Easy way to draw circle/box/sphere
    +FPrimitiveDrawInterface only provides basic draw methods (DrawSprite, DrawPoint, DrawLine, DrawMesh). However Unreal already has a collection of “advanced” draw methods for their own use. Defined in “PrimitiveDrawingUtils.cpp” and declared in “SceneManagement.h”. Check out “PrimitiveDrawingUtils.cpp” for details. Necessary files should already be included, so just call “DrawCircle” or “DrawBox”.

    +
  • +
  • +

    Draw point with world space size
    +The default FPrimitiveDrawInterface::DrawPoint function will only draw point with screen space size, but sometimes you want to give it a world space size, here’s how you can do it:

    +
  • +
+
+
+
+
void DrawPointWS (
+    FPrimitiveDrawInterface* PDI,
+    const FVector& Position,
+    const FLinearColor& Color,
+    float PointSize,
+    uint8 DepthPriorityGroup,
+    bool bScreenSpaceSize
+)
+{
+    float ScaledPointSize = PointSize;
+    if (!bScreenSpaceSize)
+    {
+        FVector PositionVS = PDI->View->ViewMatrices.GetViewMatrix().TransformPosition(Position);
+        float factor = FMath::Max(FMath::Abs(PositionVS.Z), 0.001f);
+        ScaledPointSize /= factor;
+        ScaledPointSize *= PDI->View->ViewRect.Width();
+    }
+    PDI->DrawPoint(Position, Color, ScaledPointSize, DepthPriorityGroup);
+}
+
+
+
+
+

Other Tricks for Editor Mode

+
+
    +
  • +

    It is quite common you need a viewport client to do something, and not all functions has viewport client passed in. Here is the call you can get that from anywhere:

    +
  • +
+
+
+
+
FEditorViewportClient* client = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient();
+
+
+
+
    +
  • +

    It is also quite common you want to refresh rendering for the whole viewport after the user did some edit in your tool. Use the following call:

    +
  • +
+
+
+
+
GEditor->RedrawAllViewports(true);
+
+
+
+
    +
  • +

    If the Editor Mode is not responding, or lagging behind, make sure you have "Realtime" checked in viewport.

    +
  • +
+
+
+
+031.png +
+
+
+
+
+
+ + +
+ + \ No newline at end of file diff --git a/docs/images/001.png b/docs/images/001.png new file mode 100644 index 0000000..a5aef38 Binary files /dev/null and b/docs/images/001.png differ diff --git a/docs/images/002.png b/docs/images/002.png new file mode 100644 index 0000000..4c04f5c Binary files /dev/null and b/docs/images/002.png differ diff --git a/docs/images/003.png b/docs/images/003.png new file mode 100644 index 0000000..e8c4336 Binary files /dev/null and b/docs/images/003.png differ diff --git a/docs/images/004.png b/docs/images/004.png new file mode 100644 index 0000000..43d24a8 Binary files /dev/null and b/docs/images/004.png differ diff --git a/docs/images/005.png b/docs/images/005.png new file mode 100644 index 0000000..9772915 Binary files /dev/null and b/docs/images/005.png differ diff --git a/docs/images/006.png b/docs/images/006.png new file mode 100644 index 0000000..214cf2a Binary files /dev/null and b/docs/images/006.png differ diff --git a/docs/images/007.png b/docs/images/007.png new file mode 100644 index 0000000..4f03703 Binary files /dev/null and b/docs/images/007.png differ diff --git a/docs/images/008.png b/docs/images/008.png new file mode 100644 index 0000000..b1b87e6 Binary files /dev/null and b/docs/images/008.png differ diff --git a/docs/images/009.png b/docs/images/009.png new file mode 100644 index 0000000..8f82350 Binary files /dev/null and b/docs/images/009.png differ diff --git a/docs/images/010.png b/docs/images/010.png new file mode 100644 index 0000000..e16c2d0 Binary files /dev/null and b/docs/images/010.png differ diff --git a/docs/images/011.png b/docs/images/011.png new file mode 100644 index 0000000..0c00ba1 Binary files /dev/null and b/docs/images/011.png differ diff --git a/docs/images/012.png b/docs/images/012.png new file mode 100644 index 0000000..48a8394 Binary files /dev/null and b/docs/images/012.png differ diff --git a/docs/images/013.png b/docs/images/013.png new file mode 100644 index 0000000..ddd2870 Binary files /dev/null and b/docs/images/013.png differ diff --git a/docs/images/014.png b/docs/images/014.png new file mode 100644 index 0000000..7d6e0e4 Binary files /dev/null and b/docs/images/014.png differ diff --git a/docs/images/015.png b/docs/images/015.png new file mode 100644 index 0000000..a9a94bf Binary files /dev/null and b/docs/images/015.png differ diff --git a/docs/images/016.png b/docs/images/016.png new file mode 100644 index 0000000..4d38dac Binary files /dev/null and b/docs/images/016.png differ diff --git a/docs/images/017.png b/docs/images/017.png new file mode 100644 index 0000000..1ee7864 Binary files /dev/null and b/docs/images/017.png differ diff --git a/docs/images/018.png b/docs/images/018.png new file mode 100644 index 0000000..bfe12e8 Binary files /dev/null and b/docs/images/018.png differ diff --git a/docs/images/019.png b/docs/images/019.png new file mode 100644 index 0000000..eb2e75c Binary files /dev/null and b/docs/images/019.png differ diff --git a/docs/images/020.png b/docs/images/020.png new file mode 100644 index 0000000..930af53 Binary files /dev/null and b/docs/images/020.png differ diff --git a/docs/images/021.png b/docs/images/021.png new file mode 100644 index 0000000..d33cefb Binary files /dev/null and b/docs/images/021.png differ diff --git a/docs/images/022.png b/docs/images/022.png new file mode 100644 index 0000000..fe35649 Binary files /dev/null and b/docs/images/022.png differ diff --git a/docs/images/023.png b/docs/images/023.png new file mode 100644 index 0000000..c73f0f6 Binary files /dev/null and b/docs/images/023.png differ diff --git a/docs/images/024.png b/docs/images/024.png new file mode 100644 index 0000000..de62186 Binary files /dev/null and b/docs/images/024.png differ diff --git a/docs/images/025.png b/docs/images/025.png new file mode 100644 index 0000000..c1004b4 Binary files /dev/null and b/docs/images/025.png differ diff --git a/docs/images/026.png b/docs/images/026.png new file mode 100644 index 0000000..e9e4935 Binary files /dev/null and b/docs/images/026.png differ diff --git a/docs/images/027.png b/docs/images/027.png new file mode 100644 index 0000000..e8aa13b Binary files /dev/null and b/docs/images/027.png differ diff --git a/docs/images/028.png b/docs/images/028.png new file mode 100644 index 0000000..9e96aa2 Binary files /dev/null and b/docs/images/028.png differ diff --git a/docs/images/029.png b/docs/images/029.png new file mode 100644 index 0000000..1965900 Binary files /dev/null and b/docs/images/029.png differ diff --git a/docs/images/030.png b/docs/images/030.png new file mode 100644 index 0000000..b8a3db2 Binary files /dev/null and b/docs/images/030.png differ diff --git a/docs/images/031.png b/docs/images/031.png new file mode 100644 index 0000000..41b9f36 Binary files /dev/null and b/docs/images/031.png differ diff --git a/docs/images/splash.png b/docs/images/splash.png new file mode 100644 index 0000000..58d111e Binary files /dev/null and b/docs/images/splash.png differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..986d408 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,2717 @@ + + + + + +
+ +
+ +

How to Make Tools in UE4

+ by Eric Zhang - original article: https://lxjk.github.io/2019/10/01/How-to-Make-Tools-in-U-E.html + +
+ +
+
+
+
+

This article is based on Unreal 4.17 code base, tested in Unreal 4.23.

+
+
+

This is a step by step tutorial to write tools for your Unreal project. I would assume you are familiar with Unreal already. This is NOT a tutorial for SLATE code, that deserves a tutorial for its own, and there are lots of SLATE example in Unreal already. With that said there will be some basic SLATE code in this tutorial to build UI widget, and I will try to show some different use cases for each example.

+
+
+

The example project is available in https://github.com/lxjk/ToolExample . Right click on the "ToolExample.uproject" and choose Switch Unreal Engine version to link to your engine.

+
+ +
+
+
+

Setup Editor Module

+
+
+

To make proper tools in Unreal it is almost a must to setup a custom editor module first. This will provide you an entry point for you custom tools, and also make sure your tool will not be included other than running in editor.

+
+
+

Here we create a new ToolExample project.

+
+
+

First we want to create a "ToolExampleEditor" folder and add the following files. This will be our new editor module.

+
+
+
+001.png +
+
+
+

IExampleModuleInterface.h

+
+

In this header, we first define IExampleModuleListenerInterface, a convenient interface to provide event when our module starts up or shuts down. Almost all our later tools will need to implement this interface.

+
+
+

Then we define IExampleModuleInterface, this is not necessary if you only have one editor module, but if you have more than that, this will handle event broadcasting for you. +It is required that a module inherit from IModuleInterface, so our interface will inherit from the same class.

+
+
+
IExampleModuleInterface.h
+
+
#include "ModuleManager.h"
+
+class IExampleModuleListenerInterface
+{
+public:
+    virtual void OnStartupModule() {};
+    virtual void OnShutdownModule() {};
+};
+
+class IExampleModuleInterface : public IModuleInterface
+{
+public:
+    void StartupModule() override
+    {
+        if (!IsRunningCommandlet())
+        {
+            AddModuleListeners();
+            for (int32 i = 0; i < ModuleListeners.Num(); ++i)
+            {
+                ModuleListeners[i]->OnStartupModule();
+            }
+        }
+    }
+
+    void ShutdownModule() override
+    {
+        for (int32 i = 0; i < ModuleListeners.Num(); ++i)
+        {
+            ModuleListeners[i]->OnShutdownModule();
+        }
+    }
+
+    virtual void AddModuleListeners() {};
+
+protected:
+    TArray<TSharedRef<IExampleModuleListenerInterface>> ModuleListeners;
+};
+
+
+
+
+

ToolExmampleEditor.Build.cs

+
+

This file you can copy from ToolExample.Build.cs. We added commonly used module names to dependency. Note we add "ToolExample" module here as well.

+
+
+
ToolExmampleEditor.Build.cs
+
+
PublicDependencyModuleNames.AddRange(
+            new string[] {
+                "Core",
+                "Engine",
+                "CoreUObject",
+                "InputCore",
+                "LevelEditor",
+                "Slate",
+                "EditorStyle",
+                "AssetTools",
+                "EditorWidgets",
+                "UnrealEd",
+                "BlueprintGraph",
+                "AnimGraph",
+                "ComponentVisualizers",
+                "ToolExample"
+        }
+        );
+
+
+PrivateDependencyModuleNames.AddRange(
+            new string[]
+            {
+                "Core",
+                "CoreUObject",
+                "Engine",
+                "AppFramework",
+                "SlateCore",
+                "AnimGraph",
+                "UnrealEd",
+                "KismetWidgets",
+                "MainFrame",
+                "PropertyEditor",
+                "ComponentVisualizers",
+                "ToolExample"
+            }
+            );
+
+
+
+
+

ToolExampleEditor.h & ToolExampleEditor.cpp

+
+

Here we define the actual module class, implementing IExampleModuleInterface we defined above. We include headers we need for following sections as well. Make sure the module name you use to get module is the same as the one you pass in IMPLEMENT_GAME_MODULE macro.

+
+
+
ToolExampleEditor.h
+
+
#include "UnrealEd.h"
+#include "SlateBasics.h"
+#include "SlateExtras.h"
+#include "Editor/LevelEditor/Public/LevelEditor.h"
+#include "Editor/PropertyEditor/Public/PropertyEditing.h"
+#include "IAssetTypeActions.h"
+#include "IExampleModuleInterface.h"
+
+class FToolExampleEditor : public IExampleModuleInterface
+{
+public:
+    /** IModuleInterface implementation */
+    virtual void StartupModule() override;
+    virtual void ShutdownModule() override;
+
+    virtual void AddModuleListeners() override;
+
+    static inline FToolExampleEditor& Get()
+    {
+        return FModuleManager::LoadModuleChecked< FToolExampleEditor >("ToolExampleEditor");
+    }
+
+    static inline bool IsAvailable()
+    {
+        return FModuleManager::Get().IsModuleLoaded("ToolExampleEditor");
+    }
+};
+
+
+
+
ToolExampleEditor.cpp
+
+
#include "ToolExampleEditor.h"
+#include "IExampleModuleInterface.h"
+
+IMPLEMENT_GAME_MODULE(FToolExampleEditor, ToolExampleEditor)
+
+void FToolExampleEditor::AddModuleListeners()
+{
+    // add tools later
+}
+
+void FToolExampleEditor::StartupModule()
+{
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+
+

ToolExampleEditor.Target.cs

+
+

We need to modify this file to load our module in Editor mode (Don’t change ToolExample.Target.cs), add the following:

+
+
+
ToolExampleEditor.Target.cs
+
+
ExtraModuleNames.AddRange( new string[] { "ToolExampleEditor" });
+
+
+
+
+

ToolExample.uproject

+
+

Similarly, we need to include our modules here, add the following:

+
+
+
ToolExample.uproject
+
+
{
+    "Name": "ToolExampleEditor",
+    "Type": "Editor",
+    "LoadingPhase": "PostEngineInit",
+    "AdditionalDependencies": [
+        "Engine"
+    ]
+}
+
+
+
+

Now the editor module should be setup properly.

+
+
+
+
+
+

Add Custom Menu

+
+
+

Next we are going to add a custom menu, so we can add widget in the menu to run a command or open up a window.

+
+
+

First we need to add menu extensions related functions in our editor module ToolExampleEditor:

+
+
+
ToolExampleEditor.h
+
+
public:
+    void AddMenuExtension(const FMenuExtensionDelegate &extensionDelegate, FName extensionHook, const TSharedPtr<FUICommandList> &CommandList = NULL, EExtensionHook::Position position = EExtensionHook::Before);
+    TSharedRef<FWorkspaceItem> GetMenuRoot() { return MenuRoot; };
+
+protected:
+    TSharedPtr<FExtensibilityManager> LevelEditorMenuExtensibilityManager;
+    TSharedPtr<FExtender> MenuExtender;
+
+    static TSharedRef<FWorkspaceItem> MenuRoot;
+
+    void MakePulldownMenu(FMenuBarBuilder &menuBuilder);
+    void FillPulldownMenu(FMenuBuilder &menuBuilder);
+
+
+
+

In the cpp file, define MenuRoot and add the implement all the functions. Here we will add a menu called "Example" and create 2 sections: "Section 1" and "Section 2", with extension hook name "Section_1" and "Section_2".

+
+
+
ToolExampleEditor.cpp
+
+
TSharedRef<FWorkspaceItem> FToolExampleEditor::MenuRoot = FWorkspaceItem::NewGroup(FText::FromString("Menu Root"));
+
+
+void FToolExampleEditor::AddMenuExtension(const FMenuExtensionDelegate &extensionDelegate, FName extensionHook, const TSharedPtr<FUICommandList> &CommandList, EExtensionHook::Position position)
+{
+    MenuExtender->AddMenuExtension(extensionHook, position, CommandList, extensionDelegate);
+}
+
+void FToolExampleEditor::MakePulldownMenu(FMenuBarBuilder &menuBuilder)
+{
+    menuBuilder.AddPullDownMenu(
+        FText::FromString("Example"),
+        FText::FromString("Open the Example menu"),
+        FNewMenuDelegate::CreateRaw(this, &FToolExampleEditor::FillPulldownMenu),
+        "Example",
+        FName(TEXT("ExampleMenu"))
+    );
+}
+
+void FToolExampleEditor::FillPulldownMenu(FMenuBuilder &menuBuilder)
+{
+    // just a frame for tools to fill in
+    menuBuilder.BeginSection("ExampleSection", FText::FromString("Section 1"));
+    menuBuilder.AddMenuSeparator(FName("Section_1"));
+    menuBuilder.EndSection();
+
+    menuBuilder.BeginSection("ExampleSection", FText::FromString("Section 2"));
+    menuBuilder.AddMenuSeparator(FName("Section_2"));
+    menuBuilder.EndSection();
+}
+
+
+
+

Finally in StartupModule we add the following before we call the parent function. We add our menu after "Window" menu.

+
+
+
ToolExampleEditor.cpp
+
+
void FToolExampleEditor::StartupModule()
+{
+    if (!IsRunningCommandlet())
+    {
+        FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
+        LevelEditorMenuExtensibilityManager = LevelEditorModule.GetMenuExtensibilityManager();
+        MenuExtender = MakeShareable(new FExtender);
+        MenuExtender->AddMenuBarExtension("Window", EExtensionHook::After, NULL, FMenuBarExtensionDelegate::CreateRaw(this, &FToolExampleEditor::MakePulldownMenu));
+        LevelEditorMenuExtensibilityManager->AddExtender(MenuExtender);
+    }
+    IExampleModuleInterface::StartupModule();
+}
+
+
+
+

Now if you run it you should see the custom menu get added with two sections.

+
+
+
+002.png +
+
+
+

Next we can add our first tool to register to our menu. First add two new files:

+
+
+
+003.png +
+
+
+

This class will inherit from IExampleModuleListenerInterface, and we add function to create menu entry. We also add FUICommandList, which will define and map a menu item to a function. Finally we add our only menu function MenuCommand1, this function will be called when user click on the menu item.

+
+
+
MenuTool.h
+
+
#include "ToolExampleEditor/IExampleModuleInterface.h"
+
+class MenuTool : public IExampleModuleListenerInterface, public TSharedFromThis<MenuTool>
+{
+public:
+    virtual ~MenuTool() {}
+
+    virtual void OnStartupModule() override;
+    virtual void OnShutdownModule() override;
+
+    void MakeMenuEntry(FMenuBuilder &menuBuilder);
+
+protected:
+    TSharedPtr<FUICommandList> CommandList;
+
+    void MapCommands();
+
+    // UI Command functions
+    void MenuCommand1();
+};
+
+
+
+

On the cpp side, we got a lot more to do. First we need to define LOCTEXT_NAMESPACE at the beginning, and un-define it at the end. This is required to use UI_COMMAND macro. +Then we start filling in each command, first create a FUICommandInfo member for each command in command list class, fill in RegisterCommands function by using UI_COMMAND marcro. Then in MapCommands function map each command info to a function. And of course define the command function MenuTool::MenuCommand1.

+
+
+

In OnStartupModule, we create command list, register it, map it, then register to menu extension. In this case we want our item in "Section 1", and MakeMenuEntry will be called when Unreal build the menu, in which we simply add MenuCommand1 to the menu.

+
+
+

In OnShutdownModule, we need to unregister command list.

+
+
+
MenuTool.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "MenuTool.h"
+
+#define LOCTEXT_NAMESPACE "MenuTool"
+
+class MenuToolCommands : public TCommands<MenuToolCommands>
+{
+public:
+
+    MenuToolCommands()
+        : TCommands<MenuToolCommands>(
+        TEXT("MenuTool"), // Context name for fast lookup
+        FText::FromString("Example Menu tool"), // Context name for displaying
+        NAME_None,   // No parent context
+        FEditorStyle::GetStyleSetName() // Icon Style Set
+        )
+    {
+    }
+
+    virtual void RegisterCommands() override
+    {
+        UI_COMMAND(MenuCommand1, "Menu Command 1", "Test Menu Command 1.", EUserInterfaceActionType::Button, FInputGesture());
+
+    }
+
+public:
+    TSharedPtr<FUICommandInfo> MenuCommand1;
+};
+
+void MenuTool::MapCommands()
+{
+    const auto& Commands = MenuToolCommands::Get();
+
+    CommandList->MapAction(
+        Commands.MenuCommand1,
+        FExecuteAction::CreateSP(this, &MenuTool::MenuCommand1),
+        FCanExecuteAction());
+}
+
+void MenuTool::OnStartupModule()
+{
+    CommandList = MakeShareable(new FUICommandList);
+    MenuToolCommands::Register();
+    MapAction();
+    FToolExampleEditor::Get().AddMenuExtension(
+        FMenuExtensionDelegate::CreateRaw(this, &MenuTool::MakeMenuEntry),
+        FName("Section_1"),
+        CommandList);
+}
+
+void MenuTool::OnShutdownModule()
+{
+    MenuToolCommands::Unregister();
+}
+
+void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder)
+{
+    menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand1);
+}
+
+void MenuTool::MenuCommand1()
+{
+    UE_LOG(LogClass, Log, TEXT("clicked MenuCommand1"));
+}
+
+#undef LOCTEXT_NAMESPACE
+
+
+
+

When this is all done, remember to add this tool as a listener to editor module in FToolExampleEditor::AddModuleListeners:

+
+
+
ToolExampleEditor.cpp
+
+
ModuleListeners.Add(MakeShareable(new MenuTool));
+
+
+
+

Now if you build the project, you should see your menu item in the menu. And if you click on it, it will print "clicked MenuCommand1".

+
+
+

By now you have a basic framework for tools, You can run anything you want based on a menu click.

+
+
+
+004.png +
+
+
+
+
+

Advanced Menu

+
+
+

Before we jump to window, let’s extend menu functionality for a bit, since there are a lot more you can do.

+
+
+

First if you have a lot of items, it will be good to put them in a sub menu. Let’s make two more commands MenuCommand2 and MenuCommand3. You can search for MenuCommand1 and create two more in each places, other than MakeMenuEntry, where we will add sub menu.

+
+
+

In MenuTool, we add function for sub menu:

+
+
+
MenuTool.h
+
+
void MakeSubMenu(FMenuBuilder &menuBuilder);
+
+
+
+
MenuTool.cpp
+
+
void MenuTool::MakeSubMenu(FMenuBuilder &menuBuilder)
+{
+    menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand2);
+    menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand3);
+}
+
+
+
+

Then we call AddSubMenu in MenuTool::MakeMenuEntry, after MenuCommand1 is registered so the submenu comes after that.

+
+
+
MenuTool.cpp
+
+
void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder)
+{
+    ...
+    menuBuilder.AddSubMenu(
+        FText::FromString("Sub Menu"),
+        FText::FromString("This is example sub menu"),
+        FNewMenuDelegate::CreateSP(this, &MenuTool::MakeSubMenu)
+    );
+}
+
+
+
+

Now you should see sub menu like the following:

+
+
+
+005.png +
+
+
+

Not only you can add simple menu item, you can actually add any widget into the menu. We will try to make a small tool that you can type in a textbox and click a button to set that as tags for selected actors.

+
+
+

I’m not going to go into details for each functions I used here, search them in Unreal engine and you should find plenty of use cases.

+
+
+

First we add needed member and functions, note this time we are going to use custom widget, so we don’t need to change command list. For AddTag fucntion, because it is going to be used for a button, return type have to be FReply.

+
+
+
MenuTool.h
+
+
FName TagToAdd;
+
+FReply AddTag();
+FText GetTagToAddText() const;
+void OnTagToAddTextCommited(const FText& InText, ETextCommit::Type CommitInfo);
+
+
+
+

Then we fill in those functions. If you type in a text, we save it to TagToAdd. If you click on the button, we search all selected actors and make the tag change. We wrap it around a transaction so it will support undo. To use transaction we need to include "ScopedTransaction.h".

+
+
+
MenuTool.cpp
+
+
FReply MenuTool::AddTag()
+{
+    if (!TagToAdd.IsNone())
+    {
+        const FScopedTransaction Transaction(FText::FromString("Add Tag"));
+        for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It)
+        {
+            AActor* Actor = static_cast<AActor*>(*It);
+            if (!Actor->Tags.Contains(TagToAdd))
+            {
+                Actor->Modify();
+                Actor->Tags.Add(TagToAdd);
+            }
+        }
+    }
+    return FReply::Handled();
+}
+
+FText MenuTool::GetTagToAddText() const
+{
+    return FText::FromName(TagToAdd);
+}
+
+void MenuTool::OnTagToAddTextCommited(const FText& InText, ETextCommit::Type CommitInfo)
+{
+    FString str = InText.ToString();
+    TagToAdd = FName(*str.Trim());
+}
+
+
+
+

Then in MenuTool::MakeMenuEntry, we create the widget and add it to the menu. Again I will not go into Slate code details.

+
+
+
MenuTool.cpp
+
+
void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder)
+{
+    ...
+    TSharedRef<SWidget> AddTagWidget =
+        SNew(SHorizontalBox)
+        + SHorizontalBox::Slot()
+        .AutoWidth()
+        .VAlign(VAlign_Center)
+        [
+            SNew(SEditableTextBox)
+            .MinDesiredWidth(50)
+            .Text(this, &MenuTool::GetTagToAddText)
+            .OnTextCommitted(this, &MenuTool::OnTagToAddTextCommited)
+        ]
+        + SHorizontalBox::Slot()
+        .AutoWidth()
+        .Padding(5, 0, 0, 0)
+        .VAlign(VAlign_Center)
+        [
+            SNew(SButton)
+            .Text(FText::FromString("Add Tag"))
+            .OnClicked(this, &MenuTool::AddTag)
+        ];
+
+    menuBuilder.AddWidget(AddTagWidget, FText::FromString(""));
+}
+
+
+
+

Now you have a more complex tool sit in the menu, and you can set actor tags with it:

+
+
+
+006.png +
+
+
+
+
+

Create a Tab (Window)

+
+
+

While we can do a lot in the menu, it is still more convenient and flexible if you have a window. In Unreal it is called "tab". Because creating a tab from menu is very common for tools, we will make a base class for it first.

+
+
+

Add a new file:

+
+
+
+007.png +
+
+
+

The base class is also inherit from IExampleModuleListenerInterface. In OnStartupModule we register a tab, and unregister it in OnShutdownModule. Then in MakeMenuEntry, we let FGlobalTabmanager to populate tab for this menu item. +We leave SpawnTab function to be overriden by child class to set proper widget.

+
+
+
ExampleTabToolBase.h
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ToolExampleEditor/IExampleModuleInterface.h"
+#include "TabManager.h"
+#include "SDockTab.h"
+
+class FExampleTabToolBase : public IExampleModuleListenerInterface, public TSharedFromThis< FExampleTabToolBase >
+{
+public:
+    // IPixelopusToolBase
+    virtual void OnStartupModule() override
+    {
+        Initialize();
+        FGlobalTabmanager::Get()->RegisterNomadTabSpawner(TabName, FOnSpawnTab::CreateRaw(this, &FExampleTabToolBase::SpawnTab))
+            .SetGroup(FToolExampleEditor::Get().GetMenuRoot())
+            .SetDisplayName(TabDisplayName)
+            .SetTooltipText(ToolTipText);
+    };
+
+    virtual void OnShutdownModule() override
+    {
+        FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(TabName);
+    };
+
+    // In this function set TabName/TabDisplayName/ToolTipText
+    virtual void Initialize() {};
+    virtual TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs& TabSpawnArgs) { return SNew(SDockTab); };
+
+    virtual void MakeMenuEntry(FMenuBuilder &menuBuilder)
+    {
+        FGlobalTabmanager::Get()->PopulateTabSpawnerMenu(menuBuilder, TabName);
+    };
+
+protected:
+    FName TabName;
+    FText TabDisplayName;
+    FText ToolTipText;
+};
+
+
+
+

Now we add files for tab tool. Other than the normal tool class, we also need a custom panel widget class for the tab itself.

+
+
+
+008.png +
+
+
+

Let’s look at TabTool class first, it is inherited from ExampleTabToolBase defined above.

+
+
+

We set tab name, display name and tool tips in Initialize function, and prepare the panel in SpawnTab function. Note here we send the tool object itself as a parameter when creating the panel. This is not necessary, but as an example how you can pass in an object to the widget.

+
+
+

This tab tool is added in "Section 2" in the custom menu.

+
+
+
TabTool.h
+
+
#include "ToolExampleEditor/ExampleTabToolBase.h"
+
+class TabTool : public FExampleTabToolBase
+{
+public:
+    virtual ~TabTool () {}
+    virtual void OnStartupModule() override;
+    virtual void OnShutdownModule() override;
+    virtual void Initialize() override;
+    virtual TSharedRef<SDockTab> SpawnTab(const FSpawnTabArgs& TabSpawnArgs) override;
+};
+
+
+
+
TabTool.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "TabToolPanel.h"
+#include "TabTool.h"
+
+void TabTool::OnStartupModule()
+{
+    FExampleTabToolBase::OnStartupModule();
+    FToolExampleEditor::Get().AddMenuExtension(FMenuExtensionDelegate::CreateRaw(this, &TabTool::MakeMenuEntry), FName("Section_2"));
+}
+
+void TabTool::OnShutdownModule()
+{
+    FExampleTabToolBase::OnShutdownModule();
+}
+
+void TabTool::Initialize()
+{
+    TabName = "TabTool";
+    TabDisplayName = FText::FromString("Tab Tool");
+    ToolTipText = FText::FromString("Tab Tool Window");
+}
+
+TSharedRef<SDockTab> TabTool::SpawnTab(const FSpawnTabArgs& TabSpawnArgs)
+{
+    TSharedRef<SDockTab> SpawnedTab = SNew(SDockTab)
+        .TabRole(ETabRole::NomadTab)
+        [
+            SNew(TabToolPanel)
+            .Tool(SharedThis(this))
+        ];
+
+    return SpawnedTab;
+}
+
+
+
+

Now for the pannel:

+
+
+

In the construct function we build the slate widget in ChildSlot. Here I’m add a scroll box, with a grey border inside, with a text box inside.

+
+
+
TabToolPanel.h
+
+
#include "SDockTab.h"
+#include "SDockableTab.h"
+#include "SDockTabStack.h"
+#include "SlateApplication.h"
+#include "TabTool.h"
+
+class TabToolPanel : public SCompoundWidget
+{
+    SLATE_BEGIN_ARGS(TabToolPanel)
+    {}
+    SLATE_ARGUMENT(TWeakPtr<class TabTool>, Tool)
+    SLATE_END_ARGS()
+
+    void Construct(const FArguments& InArgs);
+
+protected:
+    TWeakPtr<TabTool> tool;
+};
+
+
+
+
TabToolPanel.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "TabToolPanel.h"
+
+void TabToolPanel::Construct(const FArguments& InArgs)
+{
+    tool = InArgs._Tool;
+    if (tool.IsValid())
+    {
+        // do anything you need from tool object
+    }
+
+    ChildSlot
+    [
+        SNew(SScrollBox)
+        + SScrollBox::Slot()
+        .VAlign(VAlign_Top)
+        .Padding(5)
+        [
+            SNew(SBorder)
+            .BorderBackgroundColor(FColor(192, 192, 192, 255))
+            .Padding(15.0f)
+            [
+                SNew(STextBlock)
+                .Text(FText::FromString(TEXT("This is a tab example.")))
+            ]
+        ]
+    ];
+}
+
+
+
+

Finally remember to add this tool to editor module in FToolExampleEditor::AddModuleListeners:

+
+
+
ToolExampleEditor.cpp
+
+
ModuleListeners.Add(MakeShareable(new TabTool));
+
+
+
+

Now you can see tab tool in our custom menu:

+
+
+
+009.png +
+
+
+

When you click on it, it will populate a window you can dock anywhere as regular Unreal tab.

+
+
+
+010.png +
+
+
+
+
+

Customize Details Panel

+
+
+

Another commonly used feature is to customize the details panel for any UObject.

+
+
+

To show how it works, we will create an Actor class first in our game module "ToolExample". Add the follow file:

+
+
+
+011.png +
+
+
+

In this class, we add 2 booleans in "Options" category, and an integer in "Test" category. Remember to add "TOOLEXAMPLE_API" in front of class name to export it from game module, otherwise we cannot use it in editor module.

+
+
+
ExampleActor.h
+
+
#pragma once
+#include "ExampleActor.generated.h"
+
+UCLASS()
+class TOOLEXAMPLE_API AExampleActor : public AActor
+{
+    GENERATED_BODY()
+public:
+    UPROPERTY(EditAnywhere, Category = "Options")
+    bool bOption1 = false;
+
+    UPROPERTY(EditAnywhere, Category = "Options")
+    bool bOption2 = false;
+
+    UPROPERTY(EditAnywhere, Category = "Test")
+    int testInt = 0;
+};
+
+
+
+

Now if we load up Unreal and drag a "ExampleActor", you should see the following in the details panel:

+
+
+
+012.png +
+
+
+

If we want option 1 and option 2 to be mutually exclusive. You can have both unchecked or one of them checked, but you cannot have both checked. We want to customize this details panel, so if user check one of them, it will automatically uncheck the other.

+
+
+

Add the following files to editor module "ToolExampleEditor":

+
+
+
+013.png +
+
+
+

The details customization implements IDetailCustomization interface. In the main entry point CustomizeDetails function, we first hide original properties option 1 and option 2 (you can comment out those two lines and see how it works). Then we add our custom widget, here the "RadioButton" is purely a visual style, it has nothing to do with mutually exclusive logic. You can implement the same logic with other visuals like regular check box, buttons, etc.

+
+
+

In the widget functions for check box, IsModeRadioChecked and OnModeRadioChanged we add extra parameters "actor" and "optionIndex", so we can pass in the editing object and specify option when we construct the widget.

+
+
+
ExampleActorDetails.h
+
+
#pragma once
+#include "IDetailCustomization.h"
+
+class AExampleActor;
+
+class FExampleActorDetails : public IDetailCustomization
+{
+public:
+    /** Makes a new instance of this detail layout class for a specific detail view requesting it */
+    static TSharedRef<IDetailCustomization> MakeInstance();
+
+    /** IDetailCustomization interface */
+    virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override;
+
+protected:
+    // widget functions
+    ECheckBoxState IsModeRadioChecked(AExampleActor* actor, int optionIndex) const;
+    void OnModeRadioChanged(ECheckBoxState CheckType, AExampleActor* actor, int optionIndex);
+};
+
+
+
+
ExampleActorDetails.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleActorDetails.h"
+#include "DetailsCustomization/ExampleActor.h"
+
+TSharedRef<IDetailCustomization> FExampleActorDetails::MakeInstance()
+{
+    return MakeShareable(new FExampleActorDetails);
+}
+
+void FExampleActorDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout)
+{
+    TArray<TWeakObjectPtr<UObject>> Objects;
+    DetailLayout.GetObjectsBeingCustomized(Objects);
+    if (Objects.Num() != 1)
+    {
+        // skip customization if select more than one objects
+        return;
+    }
+    AExampleActor* actor = (AExampleActor*)Objects[0].Get();
+
+    // hide original property
+    DetailLayout.HideProperty(DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(AExampleActor, bOption1)));
+    DetailLayout.HideProperty(DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(AExampleActor, bOption2)));
+
+    // add custom widget to "Options" category
+    IDetailCategoryBuilder& OptionsCategory = DetailLayout.EditCategory("Options", FText::FromString(""), ECategoryPriority::Important);
+    OptionsCategory.AddCustomRow(FText::FromString("Options"))
+                .WholeRowContent()
+                [
+                    SNew(SHorizontalBox)
+                    + SHorizontalBox::Slot()
+                    .AutoWidth()
+                    .VAlign(VAlign_Center)
+                    [
+                        SNew(SCheckBox)
+                        .Style(FEditorStyle::Get(), "RadioButton")
+                        .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 1)
+                        .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 1)
+                        [
+                            SNew(STextBlock).Text(FText::FromString("Option 1"))
+                        ]
+                    ]
+                    + SHorizontalBox::Slot()
+                    .AutoWidth()
+                    .Padding(10.f, 0.f, 0.f, 0.f)
+                    .VAlign(VAlign_Center)
+                    [
+                        SNew(SCheckBox)
+                        .Style(FEditorStyle::Get(), "RadioButton")
+                        .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 2)
+                        .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 2)
+                        [
+                            SNew(STextBlock).Text(FText::FromString("Option 2"))
+                        ]
+                    ]
+                ];
+}
+
+ECheckBoxState FExampleActorDetails::IsModeRadioChecked(AExampleActor* actor, int optionIndex) const
+{
+    bool bFlag = false;
+    if (actor)
+    {
+        if (optionIndex == 1)
+            bFlag = actor->bOption1;
+        else if (optionIndex == 2)
+            bFlag = actor->bOption2;
+    }
+    return bFlag ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
+}
+
+void FExampleActorDetails::OnModeRadioChanged(ECheckBoxState CheckType, AExampleActor* actor, int optionIndex)
+{
+    bool bFlag = (CheckType == ECheckBoxState::Checked);
+    if (actor)
+    {
+        actor->Modify();
+        if (bFlag)
+        {
+            // clear all options first
+            actor->bOption1 = false;
+            actor->bOption2 = false;
+        }
+        if (optionIndex == 1)
+            actor->bOption1 = bFlag;
+        else if (optionIndex == 2)
+            actor->bOption2 = bFlag;
+    }
+}
+
+
+
+

Then we need to register the layout in FToolExampleEditor::StartupModule and unregister it in FToolExampleEditor::ShutdownModule

+
+
+
ToolExampleEditor.cpp
+
+
#include "DetailsCustomization/ExampleActor.h"
+#include "DetailsCustomization/ExampleActorDetails.h"
+
+void FToolExampleEditor::StartupModule()
+{
+    ...
+
+    // register custom layouts
+    {
+        static FName PropertyEditor("PropertyEditor");
+        FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(PropertyEditor);
+        PropertyModule.RegisterCustomClassLayout(AExampleActor::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FExampleActorDetails::MakeInstance));
+    }
+
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    // unregister custom layouts
+    if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
+    {
+        FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
+        PropertyModule.UnregisterCustomClassLayout(AExampleActor::StaticClass()->GetFName());
+    }
+
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+

Now you should see the customized details panel:

+
+
+
+014.png +
+
+
+
+
+

Custom Data Type

+
+
+

New Custom Data

+
+

For simple data, you can just inherit from UDataAsset class, then you can create your data object in Unreal content browser: Add New → miscellaneous → Data Asset

+
+
+

If you want to add you data to a custom category, you need to do a bit more work.

+
+
+

First we need to create a custom data type in game module (ExampleTool). We will make one with only one property.

+
+
+
+015.png +
+
+
+

We add "SourceFilePath" for future sections.

+
+
+
ExampleData.h
+
+
#pragma once
+#include "ExampleData.generated.h"
+
+UCLASS(Blueprintable)
+class UExampleData : public UObject
+{
+    GENERATED_BODY()
+
+public:
+    UPROPERTY(EditAnywhere, Category = "Properties")
+    FString ExampleString;
+
+#if WITH_EDITORONLY_DATA
+    UPROPERTY(Category = SourceAsset, VisibleAnywhere)
+    FString SourceFilePath;
+#endif
+};
+
+
+
+

Then in editor module, add the following files:

+
+
+
+016.png +
+
+
+

We first make the factory:

+
+
+
ExampleDataFactory.h
+
+
#pragma once
+#include "UnrealEd.h"
+#include "ExampleDataFactory.generated.h"
+
+UCLASS()
+class UExampleDataFactory : public UFactory
+{
+    GENERATED_UCLASS_BODY()
+public:
+    virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
+};
+
+
+
+
ExampleDataFactory.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleDataFactory.h"
+#include "CustomDataType/ExampleData.h"
+
+UExampleDataFactory::UExampleDataFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
+{
+    SupportedClass = UExampleData::StaticClass();
+    bCreateNew = true;
+    bEditAfterNew = true;
+}
+
+UObject* UExampleDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
+{
+    UExampleData* NewObjectAsset = NewObject<UExampleData>(InParent, Class, Name, Flags | RF_Transactional);
+    return NewObjectAsset;
+}
+
+
+
+

Then we make type actions, here we will pass in the asset category.

+
+
+
ExampleDataTypeActions.h
+
+
#pragma once
+#include "AssetTypeActions_Base.h"
+
+class FExampleDataTypeActions : public FAssetTypeActions_Base
+{
+public:
+    FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory);
+
+    // IAssetTypeActions interface
+    virtual FText GetName() const override;
+    virtual FColor GetTypeColor() const override;
+    virtual UClass* GetSupportedClass() const override;
+    virtual uint32 GetCategories() override;
+    // End of IAssetTypeActions interface
+
+private:
+    EAssetTypeCategories::Type MyAssetCategory;
+};
+
+
+
+
ExampleDataTypeActions.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleDataTypeActions.h"
+#include "CustomDataType/ExampleData.h"
+
+FExampleDataTypeActions::FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory)
+    : MyAssetCategory(InAssetCategory)
+{
+}
+
+FText FExampleDataTypeActions::GetName() const
+{
+    return FText::FromString("Example Data");
+}
+
+FColor FExampleDataTypeActions::GetTypeColor() const
+{
+    return FColor(230, 205, 165);
+}
+
+UClass* FExampleDataTypeActions::GetSupportedClass() const
+{
+    return UExampleData::StaticClass();
+}
+
+uint32 FExampleDataTypeActions::GetCategories()
+{
+    return MyAssetCategory;
+}
+
+
+
+

Finally we need to register type actions in editor module. We add an array CreatedAssetTypeActions to save all type actions we registered, so we can unregister them properly when module is unloaded:

+
+
+
ToolExampleEditor.h
+
+
class FToolExampleEditor : public IExampleModuleInterface
+{
+    ...
+    TArray<TSharedPtr<IAssetTypeActions>> CreatedAssetTypeActions;
+}
+
+
+
+

In StartupModule function, we create a new "Example" category, and use that to register our type action.

+
+
+
ToolExampleEditor.cpp
+
+
#include "CustomDataType/ExampleDataTypeActions.h"
+
+void FToolExampleEditor::StartupModule()
+{
+    ...
+
+    // register custom types:
+    {
+        IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
+        // add custom category
+        EAssetTypeCategories::Type ExampleCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Example")), FText::FromString("Example"));
+        // register our custom asset with example category
+        TSharedPtr<IAssetTypeActions> Action = MakeShareable(new FExampleDataTypeActions(ExampleCategory));
+        AssetTools.RegisterAssetTypeActions(Action.ToSharedRef());
+        // saved it here for unregister later
+        CreatedAssetTypeActions.Add(Action);
+    }
+
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    ...
+
+    // Unregister all the asset types that we registered
+    if (FModuleManager::Get().IsModuleLoaded("AssetTools"))
+    {
+        IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
+        for (int32 i = 0; i < CreatedAssetTypeActions.Num(); ++i)
+        {
+            AssetTools.UnregisterAssetTypeActions(CreatedAssetTypeActions[i].ToSharedRef());
+        }
+    }
+    CreatedAssetTypeActions.Empty();
+
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+

Now you will see your data in proper category.

+
+
+
+017.png +
+
+
+
+

Import Custom Data

+
+

For all the hard work we did above, we can now our data from a file, like the way you can drag and drop an PNG file to create a texture. In this case we will have a text file, with extension ".xmp", to be imported into unreal, and we just set the text from the file to "ExampleString" property.

+
+
+

To make it work with import, we actually have to disable the ability to be able to create a new data from scratch. Modify factory class as following:

+
+
+
ExampleDataFactory.h
+
+
class UExampleDataFactory : public UFactory
+{
+    ...
+
+    virtual UObject* FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn) override;
+    virtual bool FactoryCanImport(const FString& Filename) override;
+
+    // helper function
+    static void MakeExampleDataFromText(class UExampleData* Data, const TCHAR*& Buffer, const TCHAR* BufferEnd);
+};
+
+
+
+
ExampleDataFactory.cpp
+
+
UExampleDataFactory::UExampleDataFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
+{
+    Formats.Add(TEXT("xmp;Example Data"));
+    SupportedClass = UExampleData::StaticClass();
+    bCreateNew = false; // turned off for import
+    bEditAfterNew = false; // turned off for import
+    bEditorImport = true;
+    bText = true;
+}
+
+
+UObject* UExampleDataFactory::FactoryCreateText(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, const TCHAR* Type, const TCHAR*& Buffer, const TCHAR* BufferEnd, FFeedbackContext* Warn)
+{
+    FEditorDelegates::OnAssetPreImport.Broadcast(this, InClass, InParent, InName, Type);
+
+    // if class type or extension doesn't match, return
+    if (InClass != UExampleData::StaticClass() ||
+        FCString::Stricmp(Type, TEXT("xmp")) != 0)
+        return nullptr;
+
+    UExampleData* Data = CastChecked<UExampleData>(NewObject<UExampleData>(InParent, InName, Flags));
+    MakeExampleDataFromText(Data, Buffer, BufferEnd);
+
+    // save the source file path
+    Data->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, Data->GetOutermost());
+
+    FEditorDelegates::OnAssetPostImport.Broadcast(this, Data);
+
+    return Data;
+}
+
+bool UExampleDataFactory::FactoryCanImport(const FString& Filename)
+{
+    return FPaths::GetExtension(Filename).Equals(TEXT("xmp"));
+}
+
+void UExampleDataFactory::MakeExampleDataFromText(class UExampleData* Data, const TCHAR*& Buffer, const TCHAR* BufferEnd)
+{
+    Data->ExampleString = Buffer;
+}
+
+
+
+

Note we changed bCreateNew and bEditAfterNew to false. We set "SourceFilePath" so we can do reimport later. If you want to import binary file, set bText = false, and override FactoryCreateBinary function instead.

+
+
+

Now you can drag & drop a xmp file and have the content imported automatically.

+
+
+
+018.png +
+
+
+

If you want to have custom editor for the data, you can follow "Customize Details Panel" section to create custom widget. Or you can override OpenAssetEditor function in ExampleDataTypeActions, to create a complete different editor. We are not going to dive in here, search "OpenAssetEditor" in Unreal engine for examples.

+
+
+
+

Reimport

+
+

To reimport a file, we need to implement a different factory class. The implementation should be straight forward.

+
+
+
+019.png +
+
+
+
ReimportExampleDataFactory.h
+
+
#pragma once
+#include "ExampleDataFactory.h"
+#include "ReimportExampleDataFactory.generated.h"
+
+UCLASS()
+class UReimportExampleDataFactory : public UExampleDataFactory, public FReimportHandler
+{
+    GENERATED_BODY()
+
+    // Begin FReimportHandler interface
+    virtual bool CanReimport(UObject* Obj, TArray<FString>& OutFilenames) override;
+    virtual void SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths) override;
+    virtual EReimportResult::Type Reimport(UObject* Obj) override;
+    // End FReimportHandler interface
+};
+
+
+
+
ReimportExampleDataFactory.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ReimportExampleDataFactory.h"
+#include "ExampleDataFactory.h"
+#include "CustomDataType/ExampleData.h"
+
+bool UReimportExampleDataFactory::CanReimport(UObject* Obj, TArray<FString>& OutFilenames)
+{
+    UExampleData* ExampleData = Cast<UExampleData>(Obj);
+    if (ExampleData)
+    {
+        OutFilenames.Add(UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost()));
+        return true;
+    }
+    return false;
+}
+
+void UReimportExampleDataFactory::SetReimportPaths(UObject* Obj, const TArray<FString>& NewReimportPaths)
+{
+    UExampleData* ExampleData = Cast<UExampleData>(Obj);
+    if (ExampleData && ensure(NewReimportPaths.Num() == 1))
+    {
+        ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(NewReimportPaths[0], ExampleData->GetOutermost());
+    }
+}
+
+EReimportResult::Type UReimportExampleDataFactory::Reimport(UObject* Obj)
+{
+    UExampleData* ExampleData = Cast<UExampleData>(Obj);
+    if (!ExampleData)
+    {
+        return EReimportResult::Failed;
+    }
+
+    const FString Filename = UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost());
+    if (!FPaths::GetExtension(Filename).Equals(TEXT("xmp")))
+    {
+        return EReimportResult::Failed;
+    }
+
+    CurrentFilename = Filename;
+    FString Data;
+    if (FFileHelper::LoadFileToString(Data, *CurrentFilename))
+    {
+        const TCHAR* Ptr = *Data;
+        ExampleData->Modify();
+        ExampleData->MarkPackageDirty();
+
+        UExampleDataFactory::MakeExampleDataFromText(ExampleData, Ptr, Ptr + Data.Len());
+
+        // save the source file path and timestamp
+        ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, ExampleData->GetOutermost());
+    }
+
+    return EReimportResult::Succeeded;
+}
+
+
+
+

And just for fun, let’s add "Reimport" to right click menu on this asset. This is also an example for how to add more actions on specific asset type. Modify ExampleDataTypeActions class:

+
+
+
ExampleDataTypeActions.h
+
+
class FExampleDataTypeActions : public FAssetTypeActions_Base
+{
+public:
+    ...
+    virtual bool HasActions(const TArray<UObject*>& InObjects) const override { return true; }
+    virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;
+
+    void ExecuteReimport(TArray<TWeakObjectPtr<UExampleData>> Objects);
+};
+
+
+
+
ExampleDataTypeActions.cpp
+
+
void FExampleDataTypeActions::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
+{
+    auto ExampleDataImports = GetTypedWeakObjectPtrs<UExampleData>(InObjects);
+
+    MenuBuilder.AddMenuEntry(
+        FText::FromString("Reimport"),
+        FText::FromString("Reimports example data."),
+        FSlateIcon(),
+        FUIAction(
+            FExecuteAction::CreateSP(this, &FExampleDataTypeActions::ExecuteReimport, ExampleDataImports),
+            FCanExecuteAction()
+        )
+    );
+}
+
+void FExampleDataTypeActions::ExecuteReimport(TArray<TWeakObjectPtr<UExampleData>> Objects)
+{
+    for (auto ObjIt = Objects.CreateConstIterator(); ObjIt; ++ObjIt)
+    {
+        auto Object = (*ObjIt).Get();
+        if (Object)
+        {
+            FReimportManager::Instance()->Reimport(Object, /*bAskForNewFileIfMissing=*/true);
+        }
+    }
+}
+
+
+
+

Now you can reimport your custom files.

+
+
+
+020.png +
+
+
+
+
+
+

Custom Editor Mode

+
+
+

Editor Mode is probably the most powerful tool framework in Unreal. You will get and react to all user input; you can render to viewport; you can monitor any change in the scene and get Undo/Redo events. Remember you can enter a mode and paint foliage over objects? You can do the same degree of stuff in custom editor mode. Editor Mode has dedicated section in UI layout, and you can customize the widget here as well.

+
+
+
+021.png +
+
+
+

Here as an example, we will create an editor mode to do a simple task. We have an actor "ExampleTargetPoint" inherit from "TargetPoint", with a list of locations. In this editor mode we want to visualize those points. You can create new points or delete points. You can also move points around as moving normal objects. Note this is not the best way for this functionality (you can use MakeEditWidget in UPROPERTY to do this easily), but rather as a way to demonstrate how to set it up and what you can potentially do.

+
+
+

Setup Editor Mode

+
+

First we need to create an icon for our editor mode. We make an 40x40 PNG file as \Content\EditorResources\IconExampleEditorMode.png

+
+
+

Then add the following files in editor module:

+
+
+
+022.png +
+
+
+

SExampleEdModeWidget is the widget we use in "Modes" panel. Here we will just create a simple one for now. We also include a commonly used util function to get EdMode object.

+
+
+
SExampleEdModeWidget.h
+
+
#pragma once
+#include "SlateApplication.h"
+
+class SExampleEdModeWidget : public SCompoundWidget
+{
+public:
+    SLATE_BEGIN_ARGS(SExampleEdModeWidget) {}
+    SLATE_END_ARGS();
+
+    void Construct(const FArguments& InArgs);
+
+    // Util Functions
+    class FExampleEdMode* GetEdMode() const;
+};
+
+
+
+
SExampleEdModeWidget.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleEdMode.h"
+#include "SExampleEdModeWidget.h"
+
+void SExampleEdModeWidget::Construct(const FArguments& InArgs)
+{
+    ChildSlot
+    [
+        SNew(SScrollBox)
+        + SScrollBox::Slot()
+        .VAlign(VAlign_Top)
+        .Padding(5.f)
+        [
+            SNew(STextBlock)
+            .Text(FText::FromString(TEXT("This is a editor mode example.")))
+        ]
+    ];
+}
+
+FExampleEdMode* SExampleEdModeWidget::GetEdMode() const
+{
+    return (FExampleEdMode*)GLevelEditorModeTools().GetActiveMode(FExampleEdMode::EM_Example);
+}
+
+
+
+

ExampleEdModeToolkit is a middle layer between EdMode and its widget:

+
+
+
ExampleEdModeToolkit.h
+
+
#pragma once
+#include "BaseToolkit.h"
+#include "ExampleEdMode.h"
+#include "SExampleEdModeWidget.h"
+
+class FExampleEdModeToolkit: public FModeToolkit
+{
+public:
+    FExampleEdModeToolkit()
+    {
+        SAssignNew(ExampleEdModeWidget, SExampleEdModeWidget);
+    }
+
+    /** IToolkit interface */
+    virtual FName GetToolkitFName() const override { return FName("ExampleEdMode"); }
+    virtual FText GetBaseToolkitName() const override { return NSLOCTEXT("BuilderModeToolkit", "DisplayName", "Builder"); }
+    virtual class FEdMode* GetEditorMode() const override { return GLevelEditorModeTools().GetActiveMode(FExampleEdMode::EM_Example); }
+    virtual TSharedPtr<class SWidget> GetInlineContent() const override { return ExampleEdModeWidget; }
+
+private:
+    TSharedPtr<SExampleEdModeWidget> ExampleEdModeWidget;
+};
+
+
+
+

Then for the main class ExampleEdMode. Since we are only try to set it up, we will leave it mostly empty, only setting up its ID and create toolkit object. We will fill it in heavily in the next section.

+
+
+
ExampleEdMode.h
+
+
#pragma once
+#include "EditorModes.h"
+
+class FExampleEdMode : public FEdMode
+{
+public:
+    const static FEditorModeID EM_Example;
+
+    // FEdMode interface
+    virtual void Enter() override;
+    virtual void Exit() override;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "Editor/UnrealEd/Public/Toolkits/ToolkitManager.h"
+#include "ScopedTransaction.h"
+#include "ExampleEdModeToolkit.h"
+#include "ExampleEdMode.h"
+
+const FEditorModeID FExampleEdMode::EM_Example(TEXT("EM_Example"));
+
+void FExampleEdMode::Enter()
+{
+    FEdMode::Enter();
+
+    if (!Toolkit.IsValid())
+    {
+        Toolkit = MakeShareable(new FExampleEdModeToolkit);
+        Toolkit->Init(Owner->GetToolkitHost());
+    }
+}
+
+void FExampleEdMode::Exit()
+{
+    FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef());
+    Toolkit.Reset();
+
+    FEdMode::Exit();
+}
+
+
+
+

As other tools, we need a tool class to handle registration. Here we need to register both editor mode and its icon.

+
+
+
ExampleEdModeTool.h
+
+
#pragma once
+#include "ToolExampleEditor/ExampleTabToolBase.h"
+
+class ExampleEdModeTool : public FExampleTabToolBase
+{
+public:
+    virtual void OnStartupModule() override;
+    virtual void OnShutdownModule() override;
+
+    virtual ~ExampleEdModeTool() {}
+private:
+    static TSharedPtr< class FSlateStyleSet > StyleSet;
+
+    void RegisterStyleSet();
+    void UnregisterStyleSet();
+
+    void RegisterEditorMode();
+    void UnregisterEditorMode();
+};
+
+
+
+
ExampleEdModeTool.cpp
+
+
#include "ToolExampleEditor/ToolExampleEditor.h"
+#include "ExampleEdModeTool.h"
+#include "ExampleEdMode.h"
+
+#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__)
+
+TSharedPtr< FSlateStyleSet > ExampleEdModeTool::StyleSet = nullptr;
+
+void ExampleEdModeTool::OnStartupModule()
+{
+    RegisterStyleSet();
+    RegisterEditorMode();
+}
+
+void ExampleEdModeTool::OnShutdownModule()
+{
+    UnregisterStyleSet();
+    UnregisterEditorMode();
+}
+
+void ExampleEdModeTool::RegisterStyleSet()
+{
+    // Const icon sizes
+    const FVector2D Icon20x20(20.0f, 20.0f);
+    const FVector2D Icon40x40(40.0f, 40.0f);
+
+    // Only register once
+    if (StyleSet.IsValid())
+    {
+        return;
+    }
+
+    StyleSet = MakeShareable(new FSlateStyleSet("ExampleEdModeToolStyle"));
+    StyleSet->SetContentRoot(FPaths::GameDir() / TEXT("Content/EditorResources"));
+    StyleSet->SetCoreContentRoot(FPaths::GameDir() / TEXT("Content/EditorResources"));
+
+    // Spline editor
+    {
+        StyleSet->Set("ExampleEdMode", new IMAGE_BRUSH(TEXT("IconExampleEditorMode"), Icon40x40));
+        StyleSet->Set("ExampleEdMode.Small", new IMAGE_BRUSH(TEXT("IconExampleEditorMode"), Icon20x20));
+    }
+
+    FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get());
+}
+
+void ExampleEdModeTool::UnregisterStyleSet()
+{
+    if (StyleSet.IsValid())
+    {
+        FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get());
+        ensure(StyleSet.IsUnique());
+        StyleSet.Reset();
+    }
+}
+
+void ExampleEdModeTool::RegisterEditorMode()
+{
+    FEditorModeRegistry::Get().RegisterMode<FExampleEdMode>(
+        FExampleEdMode::EM_Example,
+        FText::FromString("Example Editor Mode"),
+        FSlateIcon(StyleSet->GetStyleSetName(), "ExampleEdMode", "ExampleEdMode.Small"),
+        true, 500
+        );
+}
+
+void ExampleEdModeTool::UnregisterEditorMode()
+{
+    FEditorModeRegistry::Get().UnregisterMode(FExampleEdMode::EM_Example);
+}
+
+#undef IMAGE_BRUSH
+
+
+
+

Finally as usual, we add the tool to editor module FToolExampleEditor::AddModuleListeners:

+
+
+
ToolExampleEditor.cpp
+
+
ModuleListeners.Add(MakeShareable(new ExampleEdModeTool));
+
+
+
+

Now you should see our custom editor mode show up in "Modes" panel.

+
+
+
+023.png +
+
+
+
+

Render and Click

+
+

With the basic framework ready, we can actually start implementing tool logic. First we make ExampleTargetPoint class in game module. This actor holds points data, and is what our tool will be operating on. Again remember to export the class with TOOLEXAMPLE_API.

+
+
+
+024.png +
+
+
+
ExampleTargetPoint.h
+
+
#pragma once
+#include "Engine/Targetpoint.h"
+#include "ExampleTargetPoint.generated.h"
+
+UCLASS()
+class TOOLEXAMPLE_API AExampleTargetPoint : public ATargetPoint
+{
+    GENERATED_BODY()
+
+public:
+    UPROPERTY(EditAnywhere, Category = "Points")
+    TArray<FVector> Points;
+};
+
+
+
+

Now we modify ExampleEdMode to add functions to add point, remove point, and select point. We also save our current selection in variable, here we use weak object pointer to handle the case if the actor is removed.

+
+
+

For adding point, we only allow that when you have exactly on ExampleTargetPoint actor selected in editor. For removing point, we simply remove the current selected point if there is any. If you select any point, we will deselect all actors and select the actor associated with that point.

+
+
+

Note that we put FScopedTransaction, and called Modify() function whenever we modify data we need to save. This will make sure undo/redo is properly handled.

+
+
+
ExampleEdMode.h
+
+
...
+class AExampleTargetPoint;
+
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    void AddPoint();
+    bool CanAddPoint() const;
+    void RemovePoint();
+    bool CanRemovePoint() const;
+    bool HasValidSelection() const;
+    void SelectPoint(AExampleTargetPoint* actor, int32 index);
+
+    TWeakObjectPtr<AExampleTargetPoint> currentSelectedTarget;
+    int32 currentSelectedIndex = -1;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
void FExampleEdMode::Enter()
+{
+    ...
+
+    // reset
+    currentSelectedTarget = nullptr;
+    currentSelectedIndex = -1;
+}
+
+AExampleTargetPoint* GetSelectedTargetPointActor()
+{
+    TArray<UObject*> selectedObjects;
+    GEditor->GetSelectedActors()->GetSelectedObjects(selectedObjects);
+    if (selectedObjects.Num() == 1)
+    {
+        return Cast<AExampleTargetPoint>(selectedObjects[0]);
+    }
+    return nullptr;
+}
+
+void FExampleEdMode::AddPoint()
+{
+    AExampleTargetPoint* actor = GetSelectedTargetPointActor();
+    if (actor)
+    {
+        const FScopedTransaction Transaction(FText::FromString("Add Point"));
+
+        // add new point, slightly in front of camera
+        FEditorViewportClient* client = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient();
+        FVector newPoint = client->GetViewLocation() + client->GetViewRotation().Vector() * 50.f;
+        actor->Modify();
+        actor->Points.Add(newPoint);
+        // auto select this new point
+        SelectPoint(actor, actor->Points.Num() - 1);
+    }
+}
+
+bool FExampleEdMode::CanAddPoint() const
+{
+    return GetSelectedTargetPointActor() != nullptr;
+}
+
+void FExampleEdMode::RemovePoint()
+{
+    if (HasValidSelection())
+    {
+        const FScopedTransaction Transaction(FText::FromString("Remove Point"));
+
+        currentSelectedTarget->Modify();
+        currentSelectedTarget->Points.RemoveAt(currentSelectedIndex);
+        // deselect the point
+        SelectPoint(nullptr, -1);
+    }
+}
+
+bool FExampleEdMode::CanRemovePoint() const
+{
+    return HasValidSelection();
+}
+
+bool FExampleEdMode::HasValidSelection() const
+{
+    return currentSelectedTarget.IsValid() && currentSelectedIndex >= 0 && currentSelectedIndex < currentSelectedTarget->Points.Num();
+}
+
+void FExampleEdMode::SelectPoint(AExampleTargetPoint* actor, int32 index)
+{
+    currentSelectedTarget = actor;
+    currentSelectedIndex = index;
+
+    // select this actor only
+    if (currentSelectedTarget.IsValid())
+    {
+        GEditor->SelectNone(true, true);
+        GEditor->SelectActor(currentSelectedTarget.Get(), true, true);
+    }
+}
+
+
+
+

Now we have functionality ready, we still need to hook it up with UI. Modify to SExampleEdModeWidget add "Add" and "Remove" button, and we will check "CanAddPoint" and "CanRemovePoint" to determine if the button should be enabled.

+
+
+
SExampleEdModeWidget.h
+
+
class SExampleEdModeWidget : public SCompoundWidget
+{
+public:
+    ...
+    FReply OnAddPoint();
+    bool CanAddPoint() const;
+    FReply OnRemovePoint();
+    bool CanRemovePoint() const;
+};
+
+
+
+
SExampleEdModeWidget.cpp
+
+
void SExampleEdModeWidget::Construct(const FArguments& InArgs)
+{
+    ChildSlot
+    [
+        SNew(SScrollBox)
+        + SScrollBox::Slot()
+        .VAlign(VAlign_Top)
+        .Padding(5.f)
+        [
+            SNew(SVerticalBox)
+            + SVerticalBox::Slot()
+            .AutoHeight()
+            .Padding(0.f, 5.f, 0.f, 0.f)
+            [
+                SNew(STextBlock)
+                .Text(FText::FromString(TEXT("This is a editor mode example.")))
+            ]
+            + SVerticalBox::Slot()
+            .AutoHeight()
+            .Padding(0.f, 5.f, 0.f, 0.f)
+            [
+                SNew(SHorizontalBox)
+                + SHorizontalBox::Slot()
+                .AutoWidth()
+                .Padding(2, 0, 0, 0)
+                .VAlign(VAlign_Center)
+                [
+                    SNew(SButton)
+                    .Text(FText::FromString("Add"))
+                    .OnClicked(this, &SExampleEdModeWidget::OnAddPoint)
+                    .IsEnabled(this, &SExampleEdModeWidget::CanAddPoint)
+                ]
+                + SHorizontalBox::Slot()
+                .AutoWidth()
+                .VAlign(VAlign_Center)
+                .Padding(0, 0, 2, 0)
+                [
+                    SNew(SButton)
+                    .Text(FText::FromString("Remove"))
+                    .OnClicked(this, &SExampleEdModeWidget::OnRemovePoint)
+                    .IsEnabled(this, &SExampleEdModeWidget::CanRemovePoint)
+                ]
+            ]
+        ]
+    ];
+}
+
+FReply SExampleEdModeWidget::OnAddPoint()
+{
+    GetEdMode()->AddPoint();
+    return FReply::Handled();
+}
+
+bool SExampleEdModeWidget::CanAddPoint() const
+{
+    return GetEdMode()->CanAddPoint();
+}
+
+FReply SExampleEdModeWidget::OnRemovePoint()
+{
+    GetEdMode()->RemovePoint();
+    return FReply::Handled();
+}
+
+bool SExampleEdModeWidget::CanRemovePoint() const
+{
+    return GetEdMode()->CanRemovePoint();
+}
+
+
+
+

Now if you launch the editor, you should be able to drag in an "Example Target Point", switch to our editor mode, select that target point and add new points from the editor mode UI. However it is not visualized in the viewport yet, and you cannot click and select point. We will work on that next.

+
+
+

To be able to click in editor and select something, we need to define a HitProxy struct. When we render the points, we render with this hit proxy along with some data attached to it. Then when we get the click event, we can retrieve those data back from the proxy and know what we clicked on.

+
+
+

Back to ExampleEdMode, we define HExamplePointProxy with a reference object (the ExampleTargetPoint actor) and the point index, and we add Render and HandleClick override function.

+
+
+
ExampleEdMode.h
+
+
struct HExamplePointProxy : public HHitProxy
+{
+    DECLARE_HIT_PROXY();
+
+    HExamplePointProxy(UObject* InRefObject, int32 InIndex)
+        : HHitProxy(HPP_UI), RefObject(InRefObject), Index(InIndex)
+    {}
+
+    UObject* RefObject;
+    int32 Index;
+};
+
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
+    virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) override;
+};
+
+
+
+

Then in cpp file, we use macro IMPLEMENT_HIT_PROXY to implement the proxy. In Render we simply loops through all ExampleTargetPoint actor and draw all the points (and a line to the actor itself), we choose a different color if this is the current selected point. We set hit proxy for each point before drawing and clears it immediately afterwards (this is important so the proxy doesn’t leak through to other draws). In HandleClick, we test hit proxy and select point if we have a valid hit. We don’t check mouse button here, so you can select with left/right/middle click.

+
+
+
ExampleEdMode.cpp
+
+
IMPLEMENT_HIT_PROXY(HExamplePointProxy, HHitProxy);
+...
+
+void FExampleEdMode::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
+{
+    const FColor normalColor(200, 200, 200);
+    const FColor selectedColor(255, 128, 0);
+
+    UWorld* World = GetWorld();
+    for (TActorIterator<AExampleTargetPoint> It(World); It; ++It)
+    {
+        AExampleTargetPoint* actor = (*It);
+        if (actor)
+        {
+            FVector actorLoc = actor->GetActorLocation();
+            for (int i = 0; i < actor->Points.Num(); ++i)
+            {
+                bool bSelected = (actor == currentSelectedTarget && i == currentSelectedIndex);
+                const FColor& color = bSelected ? selectedColor : normalColor;
+                // set hit proxy and draw
+                PDI->SetHitProxy(new HExamplePointProxy(actor, i));
+                PDI->DrawPoint(actor->Points[i], color, 15.f, SDPG_Foreground);
+                PDI->DrawLine(actor->Points[i], actorLoc, color, SDPG_Foreground);
+                PDI->SetHitProxy(NULL);
+            }
+        }
+    }
+
+    FEdMode::Render(View, Viewport, PDI);
+}
+
+bool FExampleEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click)
+{
+    bool isHandled = false;
+
+    if (HitProxy)
+    {
+        if (HitProxy->IsA(HExamplePointProxy::StaticGetType()))
+        {
+            isHandled = true;
+            HExamplePointProxy* examplePointProxy = (HExamplePointProxy*)HitProxy;
+            AExampleTargetPoint* actor = Cast<AExampleTargetPoint>(examplePointProxy->RefObject);
+            int32 index = examplePointProxy->Index;
+            if (actor && index >= 0 && index < actor->Points.Num())
+            {
+                SelectPoint(actor, index);
+            }
+        }
+    }
+
+    return isHandled;
+}
+
+
+
+

With all of these you can start adding/removing points in the editor:

+
+
+
+025.png +
+
+
+
+

Use Transform Widget

+
+

The next mission is to be able to move point around in editor like moving any other actor. Go back to ExampleEdMode, this time we need to add support for custom transform widget, and handle InputDelta event. In InputDelta function, we don’t use FScopedTransaction because undo/redo is already handled for this function. We still need to call Modify() though.

+
+
+
ExampleEdMode.h
+
+
...
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override;
+    virtual bool ShowModeWidgets() const override;
+    virtual bool ShouldDrawWidget() const override;
+    virtual bool UsesTransformWidget() const override;
+    virtual FVector GetWidgetLocation() const override;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
bool FExampleEdMode::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale)
+{
+    if (InViewportClient->GetCurrentWidgetAxis() == EAxisList::None)
+    {
+        return false;
+    }
+
+    if (HasValidSelection())
+    {
+        if (!InDrag.IsZero())
+        {
+            currentSelectedTarget->Modify();
+            currentSelectedTarget->Points[currentSelectedIndex] += InDrag;
+        }
+        return true;
+    }
+
+    return false;
+}
+
+bool FExampleEdMode::ShowModeWidgets() const
+{
+    return true;
+}
+
+bool FExampleEdMode::ShouldDrawWidget() const
+{
+    return true;
+}
+
+bool FExampleEdMode::UsesTransformWidget() const
+{
+    return true;
+}
+
+FVector FExampleEdMode::GetWidgetLocation() const
+{
+    if (HasValidSelection())
+    {
+        return currentSelectedTarget->Points[currentSelectedIndex];
+    }
+    return FEdMode::GetWidgetLocation();
+}
+
+
+
+

Now you should have a transform widget to move your points around:

+
+
+
+026.png +
+
+
+
+
virtual bool GetCustomDrawingCoordinateSystem(FMatrix& InMatrix, void* InData) override;
+virtual bool GetCustomInputCoordinateSystem(FMatrix& InMatrix, void* InData) override;
+
+
+
+
+

Key input support, right click menu, and others

+
+

Next we will add some other common features: when we have a point selected, we want to hit delete button and remove it. Also we want to have a menu generated when you right click on a point, showing the point index, and an option to delete it.

+
+
+

Remember in the "Menu Tool" tutorial, in order to make a menu, we would need a UI command list, here we will do the same thing. We also override InputKey function to handle input. Though we can simply call functions based on which key is pressed, since we have the same functionality in the menu, we will route the input through the UI command list instead. (when we define UI Commands, we pass in a key in FInputGesture)

+
+
+

Finally we will modify HandleClick function to generate context menu when we right click on a point.

+
+
+
ExampleEdMode.h
+
+
...
+class FExampleEdMode : public FEdMode
+{
+public:
+    ...
+    FExampleEdMode();
+    ~FExampleEdMode();
+
+    virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) override;
+
+    TSharedPtr<FUICommandList> ExampleEdModeActions;
+    void MapCommands();
+    TSharedPtr<SWidget> GenerateContextMenu(FEditorViewportClient* InViewportClient) const;
+};
+
+
+
+
ExampleEdMode.cpp
+
+
class ExampleEditorCommands : public TCommands<ExampleEditorCommands>
+{
+public:
+    ExampleEditorCommands() : TCommands <ExampleEditorCommands>
+        (
+            "ExampleEditor",    // Context name for fast lookup
+            FText::FromString(TEXT("Example Editor")),  // context name for displaying
+            NAME_None,  // Parent
+            FEditorStyle::GetStyleSetName()
+            )
+    {
+    }
+
+#define LOCTEXT_NAMESPACE ""
+    virtual void RegisterCommands() override
+    {
+        UI_COMMAND(DeletePoint, "Delete Point", "Delete the currently selected point.", EUserInterfaceActionType::Button, FInputGesture(EKeys::Delete));
+    }
+#undef LOCTEXT_NAMESPACE
+
+public:
+    TSharedPtr<FUICommandInfo> DeletePoint;
+};
+
+
+FExampleEdMode::FExampleEdMode()
+{
+    ExampleEditorCommands::Register();
+    ExampleEdModeActions = MakeShareable(new FUICommandList);
+}
+
+FExampleEdMode::~FExampleEdMode()
+{
+    ExampleEditorCommands::Unregister();
+}
+
+void FExampleEdMode::MapCommands()
+{
+    const auto& Commands = ExampleEditorCommands::Get();
+
+    ExampleEdModeActions->MapAction(
+        Commands.DeletePoint,
+        FExecuteAction::CreateSP(this, &FExampleEdMode::RemovePoint),
+        FCanExecuteAction::CreateSP(this, &FExampleEdMode::CanRemovePoint));
+}
+
+bool FExampleEdMode::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event)
+{
+    bool isHandled = false;
+
+    if (!isHandled && Event == IE_Pressed)
+    {
+        isHandled = ExampleEdModeActions->ProcessCommandBindings(Key, FSlateApplication::Get().GetModifierKeys(), false);
+    }
+
+    return isHandled;
+}
+
+TSharedPtr<SWidget> FExampleEdMode::GenerateContextMenu(FEditorViewportClient* InViewportClient) const
+{
+    FMenuBuilder MenuBuilder(true, NULL);
+
+    MenuBuilder.PushCommandList(ExampleEdModeActions.ToSharedRef());
+    MenuBuilder.BeginSection("Example Section");
+    if (HasValidSelection())
+    {
+        // add label for point index
+        TSharedRef<SWidget> LabelWidget =
+            SNew(STextBlock)
+            .Text(FText::FromString(FString::FromInt(currentSelectedIndex)))
+            .ColorAndOpacity(FLinearColor::Green);
+        MenuBuilder.AddWidget(LabelWidget, FText::FromString(TEXT("Point Index: ")));
+        MenuBuilder.AddMenuSeparator();
+        // add delete point entry
+        MenuBuilder.AddMenuEntry(ExampleEditorCommands::Get().DeletePoint);
+    }
+    MenuBuilder.EndSection();
+    MenuBuilder.PopCommandList();
+
+    TSharedPtr<SWidget> MenuWidget = MenuBuilder.MakeWidget();
+    return MenuWidget;
+}
+
+
+bool FExampleEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click)
+{
+    ...
+
+    if (HitProxy && isHandled && Click.GetKey() == EKeys::RightMouseButton)
+    {
+        TSharedPtr<SWidget> MenuWidget = GenerateContextMenu(InViewportClient);
+        if (MenuWidget.IsValid())
+        {
+            FSlateApplication::Get().PushMenu(
+                Owner->GetToolkitHost()->GetParentWidget(),
+                FWidgetPath(),
+                MenuWidget.ToSharedRef(),
+                FSlateApplication::Get().GetCursorPos(),
+                FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu));
+        }
+    }
+
+    return isHandled;
+}
+
+
+
+

The following is the result:

+
+
+
+027.png +
+
+
+

There are other virtual functions from FEdMode that can be very helpful. I’ll list some of them here:

+
+
+
+
    virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override;
+    virtual bool CapturedMouseMove(FEditorViewportClient* InViewportClient, FViewport* InViewport, int32 InMouseX, int32 InMouseY) override;
+    virtual bool StartTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) override;
+    virtual bool EndTracking(FEditorViewportClient* InViewportClient, FViewport* InViewport) override;
+    virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) override;
+    virtual void PostUndo() override;
+    virtual void ActorsDuplicatedNotify(TArray<AActor*>& PreDuplicateSelection, TArray<AActor*>& PostDuplicateSelection, bool bOffsetLocations) override;
+    virtual void ActorMoveNotify() override;
+    virtual void ActorSelectionChangeNotify() override;
+    virtual void MapChangeNotify() override;
+    virtual void SelectionChanged() override;
+
+
+
+
+
+
+

Custom Project Settings

+
+
+

Remember you can you go to Edit → Project Settings in Unreal editor to change various game/editor settings? You can add your custom settings to this window as well.

+
+
+

First we create a settings object. In this example we will create it in editor module, you can create in game module as well, just remember to export it with proper macro. +In the UCLASS macro, we need specify which .ini file to write to. You can use existing .ini file like "Game" or "Editor". In this case we want this setting to be per user and not shared on source control, so we create a new ini file. +For each UPROPERTY that you want to include in the settings, mark it with "config".

+
+
+
+028.png +
+
+
+
ExampleSettings.h
+
+
#pragma once
+#include "ExampleSettings.generated.h"
+
+UCLASS(config = EditorUserSettings, defaultconfig)
+class UExampleSettings : public UObject
+{
+    GENERATED_BODY()
+
+    UPROPERTY(EditAnywhere, config, Category = Test)
+    bool bTest = false;
+};
+
+
+
+
ToolExampleEditor.cpp
+
+
...
+#include "ISettingsModule.h"
+#include "Developer/Settings/Public/ISettingsContainer.h"
+#include "CustomProjectSettings/ExampleSettings.h"
+
+void FToolExampleEditor::StartupModule()
+{
+    ...
+    // register settings:
+    {
+        ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
+        if (SettingsModule)
+        {
+            TSharedPtr<ISettingsContainer> ProjectSettingsContainer = SettingsModule->GetContainer("Project");
+            ProjectSettingsContainer->DescribeCategory("ExampleCategory", FText::FromString("Example Category"), FText::FromString("Example settings description text here"));
+
+            SettingsModule->RegisterSettings("Project", "ExampleCategory", "ExampleSettings",
+                FText::FromString("Example Settings"),
+                FText::FromString("Configure Example Settings"),
+                GetMutableDefault<UExampleSettings>()
+            );
+        }
+    }
+
+    IExampleModuleInterface::StartupModule();
+}
+
+void FToolExampleEditor::ShutdownModule()
+{
+    ...
+    // unregister settings
+    ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");
+    if (SettingsModule)
+    {
+        SettingsModule->UnregisterSettings("Project", "ExampleCategory", "ExampleSettings");
+    }
+
+    IExampleModuleInterface::ShutdownModule();
+}
+
+
+
+

Now you should see your custom settings in "Project Settings" window. And when you change it, you should see DefaultEditorUserSettings.ini created in \ToolExample\Config

+
+
+
+029.png +
+
+
+

To get access to this settings, do the following:

+
+
+
+
const UExampleSettings* ExampleSettings = GetDefault<UExampleSettings>();
+if(ExampleSettings && ExampleSettings->bTest)
+    // do something
+
+
+
+
+
+

Tricks

+
+
+

Use Widget Reflector

+
+

The best way to learn SLATE and Unreal tools, is to use Widget Reflector. In Window → Developer Tool → Widget Reflector to launch the reflector. Click on "Pick Live Widget" and mouse over the widget you want to see, then hit "ESC" to freeze.

+
+
+

For example we can mouse over our editor mode widget, and you can see the structure showing in the reflector window. You can click on the file and it will take you to the exact place that widget is constructed. This is powerful tool to debug your widget or to learn how Unreal build their widget.

+
+
+
+030.png +
+
+
+
+

Is my tool running in the editor or game?

+
+

There 3 conditions that your tool is running:

+
+
+
    +
  1. +

    Editor: game not started, you can do all normal editing.

    +
  2. +
  3. +

    Game: game started, cannot do any editing.

    +
  4. +
  5. +

    Simulate: either hit “Simulate” or hit “Play” then “Eject”, game started and you can do limited editing. +Here is how you can determine which state you are in:

    +
  6. +
+
+ ++++++ + + + + + + + + + + + + + + + + + + + + +

Editor

Game

Simulate

FApp::IsGame()

false

true

true

Cast<UEditorEngine>(GEngine)→bIsSimulatingInEditor

false

false

true

+
+

Note: this do NOT work in SLATE call (any UI tick for example), because that is in SLATE world.

+
+
+
+

Useful UPROPERTY() meta marker

+
+
    +
  • +

    MakeEditWidget: If you just need to visualize a point in the level and be able to drag it around, this is the quick way to do it. It works for FVector or FTransform, and it works with TArray of those as well.
    +example: UPROPERTY(meta = (MakeEditWidget = true))

    +
  • +
  • +

    DisplayName, ToolTip: Useful if you want to have a different display name than the variable name; or if you want add a mouse over tooltip. There are plenty of examples in Unreal code base.

    +
  • +
  • +

    ClampMin, ClampMax, UIMin, UIMax: You can specify a range for the value that can be input for this field.
    +example: UPROPERTY(meta = (ClampMin = "0", ClampMax = "180"))

    +
  • +
  • +

    EditCondition: You can specify a bool to determine whether this field is editable.
    +example: UPROPERTY(meta = (EditCondition = "bIsThisFieldEnabled")))

    +
  • +
+
+
+

For a complete list, search for ObjectMacros.h in Unreal code base.

+
+
+
+

Make custom Animation Blueprint Node

+
+

To make a custom Animation Blueprint Node, you need to first inherit from FAnimNode_Base class in game module, this class will process animation pose at runtime.

+
+
+

Then in the editor module, inherit from UAnimGraphNode_Base class, and define how you want this node to be in editor.

+
+
+
+

Debug Draw Tricks

+
+
    +
  • +

    Easy way to draw circle/box/sphere
    +FPrimitiveDrawInterface only provides basic draw methods (DrawSprite, DrawPoint, DrawLine, DrawMesh). However Unreal already has a collection of “advanced” draw methods for their own use. Defined in “PrimitiveDrawingUtils.cpp” and declared in “SceneManagement.h”. Check out “PrimitiveDrawingUtils.cpp” for details. Necessary files should already be included, so just call “DrawCircle” or “DrawBox”.

    +
  • +
  • +

    Draw point with world space size
    +The default FPrimitiveDrawInterface::DrawPoint function will only draw point with screen space size, but sometimes you want to give it a world space size, here’s how you can do it:

    +
  • +
+
+
+
+
void DrawPointWS (
+    FPrimitiveDrawInterface* PDI,
+    const FVector& Position,
+    const FLinearColor& Color,
+    float PointSize,
+    uint8 DepthPriorityGroup,
+    bool bScreenSpaceSize
+)
+{
+    float ScaledPointSize = PointSize;
+    if (!bScreenSpaceSize)
+    {
+        FVector PositionVS = PDI->View->ViewMatrices.GetViewMatrix().TransformPosition(Position);
+        float factor = FMath::Max(FMath::Abs(PositionVS.Z), 0.001f);
+        ScaledPointSize /= factor;
+        ScaledPointSize *= PDI->View->ViewRect.Width();
+    }
+    PDI->DrawPoint(Position, Color, ScaledPointSize, DepthPriorityGroup);
+}
+
+
+
+
+

Other Tricks for Editor Mode

+
+
    +
  • +

    It is quite common you need a viewport client to do something, and not all functions has viewport client passed in. Here is the call you can get that from anywhere:

    +
  • +
+
+
+
+
FEditorViewportClient* client = (FEditorViewportClient*)GEditor->GetActiveViewport()->GetClient();
+
+
+
+
    +
  • +

    It is also quite common you want to refresh rendering for the whole viewport after the user did some edit in your tool. Use the following call:

    +
  • +
+
+
+
+
GEditor->RedrawAllViewports(true);
+
+
+
+
    +
  • +

    If the Editor Mode is not responding, or lagging behind, make sure you have "Realtime" checked in viewport.

    +
  • +
+
+
+
+031.png +
+
+
+
+
+
+ + +
+ + \ No newline at end of file