├── .gitignore ├── Config ├── DefaultEditor.ini ├── DefaultEngine.ini └── DefaultGame.ini ├── Content └── EditorResources │ └── IconExampleEditorMode.png ├── LICENSE.md ├── README.md ├── Source ├── ToolExample.Target.cs ├── ToolExample │ ├── CustomDataType │ │ └── ExampleData.h │ ├── DetailsCustomization │ │ └── ExampleActor.h │ ├── EditorMode │ │ └── ExampleTargetPoint.h │ ├── ToolExample.Build.cs │ ├── ToolExample.cpp │ ├── ToolExample.h │ ├── ToolExampleGameModeBase.cpp │ └── ToolExampleGameModeBase.h ├── ToolExampleEditor.Target.cs └── ToolExampleEditor │ ├── CustomDataType │ ├── ExampleDataFactory.cpp │ ├── ExampleDataFactory.h │ ├── ExampleDataTypeActions.cpp │ ├── ExampleDataTypeActions.h │ ├── ReimportExampleDataFactory.cpp │ ├── ReimportExampleDataFactory.h │ ├── XMPAssetTypeActions_Base.cpp │ ├── XMPAssetTypeActions_Base.h │ ├── XMPImportFactory.cpp │ └── XMPImportFactory.h │ ├── CustomProjectSettings │ └── ExampleSettings.h │ ├── DetailsCustomization │ ├── ExampleActorDetails.cpp │ └── ExampleActorDetails.h │ ├── EditorMode │ ├── ExampleEdMode.cpp │ ├── ExampleEdMode.h │ ├── ExampleEdModeTool.cpp │ ├── ExampleEdModeTool.h │ ├── ExampleEdModeToolkit.h │ ├── SExampleEdModeWidget.cpp │ └── SExampleEdModeWidget.h │ ├── ExampleTabToolBase.h │ ├── IExampleModuleInterface.h │ ├── MenuTool │ ├── MenuTool.cpp │ └── MenuTool.h │ ├── TabTool │ ├── TabTool.cpp │ ├── TabTool.h │ ├── TabToolPanel.cpp │ └── TabToolPanel.h │ ├── ToolExampleEditor.Build.cs │ ├── ToolExampleEditor.cpp │ └── ToolExampleEditor.h ├── ToolExample.uproject └── docs ├── How to Make Tools in UE4 - Eric's Blog.html ├── images ├── 001.png ├── 002.png ├── 003.png ├── 004.png ├── 005.png ├── 006.png ├── 007.png ├── 008.png ├── 009.png ├── 010.png ├── 011.png ├── 012.png ├── 013.png ├── 014.png ├── 015.png ├── 016.png ├── 017.png ├── 018.png ├── 019.png ├── 020.png ├── 021.png ├── 022.png ├── 023.png ├── 024.png ├── 025.png ├── 026.png ├── 027.png ├── 028.png ├── 029.png ├── 030.png ├── 031.png └── splash.png └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .ignore 2 | ToolExample.sln 3 | ToolExample.code-workspace 4 | .vs/ 5 | .vscode/ 6 | .idea/ 7 | Binaries/ 8 | Intermediate/ 9 | Saved/ 10 | .idea/ 11 | 12 | Config/DefaultEditorUserSettings.ini 13 | .vsconfig 14 | Config/DefaultInput.ini 15 | .obsidian/ 16 | -------------------------------------------------------------------------------- /Config/DefaultEditor.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/Config/DefaultEditor.ini -------------------------------------------------------------------------------- /Config/DefaultEngine.ini: -------------------------------------------------------------------------------- 1 | [URL] 2 | 3 | [/Script/EngineSettings.GameMapsSettings] 4 | EditorStartupMap= 5 | GameDefaultMap= 6 | GlobalDefaultGameMode="/Script/ToolExample.ToolExampleGameMode" 7 | 8 | [/Script/HardwareTargeting.HardwareTargetingSettings] 9 | TargetedHardwareClass=Desktop 10 | AppliedTargetedHardwareClass=Desktop 11 | DefaultGraphicsPerformance=Maximum 12 | AppliedDefaultGraphicsPerformance=Maximum 13 | 14 | [/Script/Engine.PhysicsSettings] 15 | DefaultGravityZ=-980.000000 16 | DefaultTerminalVelocity=4000.000000 17 | DefaultFluidFriction=0.300000 18 | SimulateScratchMemorySize=262144 19 | RagdollAggregateThreshold=4 20 | TriangleMeshTriangleMinAreaThreshold=5.000000 21 | bEnableAsyncScene=False 22 | bEnableShapeSharing=False 23 | bEnablePCM=True 24 | bEnableStabilization=False 25 | bWarnMissingLocks=True 26 | bEnable2DPhysics=False 27 | LockedAxis=Invalid 28 | DefaultDegreesOfFreedom=Full3D 29 | BounceThresholdVelocity=200.000000 30 | FrictionCombineMode=Average 31 | RestitutionCombineMode=Average 32 | MaxAngularVelocity=3600.000000 33 | MaxDepenetrationVelocity=0.000000 34 | ContactOffsetMultiplier=0.020000 35 | MinContactOffset=2.000000 36 | MaxContactOffset=8.000000 37 | bSimulateSkeletalMeshOnDedicatedServer=True 38 | DefaultShapeComplexity=CTF_UseSimpleAndComplex 39 | bDefaultHasComplexCollision=True 40 | bSuppressFaceRemapTable=False 41 | bSupportUVFromHitResults=False 42 | bDisableActiveActors=False 43 | bDisableCCD=False 44 | bEnableEnhancedDeterminism=False 45 | MaxPhysicsDeltaTime=0.033333 46 | bSubstepping=False 47 | bSubsteppingAsync=False 48 | MaxSubstepDeltaTime=0.016667 49 | MaxSubsteps=6 50 | SyncSceneSmoothingFactor=0.000000 51 | AsyncSceneSmoothingFactor=0.990000 52 | InitialAverageFrameRate=0.016667 53 | PhysXTreeRebuildRate=10 54 | 55 | [/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] 56 | bEnablePlugin=True 57 | bAllowNetworkConnection=True 58 | SecurityToken=6607BEA0438D48653A04889817D4749C 59 | bIncludeInShipping=False 60 | bAllowExternalStartInShipping=False 61 | bCompileAFSProject=False 62 | bUseCompression=False 63 | bLogFiles=False 64 | bReportStats=False 65 | ConnectionType=USBOnly 66 | bUseManualIPAddress=False 67 | ManualIPAddress= 68 | 69 | -------------------------------------------------------------------------------- /Config/DefaultGame.ini: -------------------------------------------------------------------------------- 1 | [/Script/EngineSettings.GeneralProjectSettings] 2 | ProjectID=C0A7B37A40F5020C10F1B6809AE1B5E1 3 | 4 | -------------------------------------------------------------------------------- /Content/EditorResources/IconExampleEditorMode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/Content/EditorResources/IconExampleEditorMode.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Eric Zhang 4 | This code includes modifications by Scott Kirvan. Modifications (c) Scott Kirvan 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [ScottKirvan/ToolExample](https://github.com/ScottKirvan/ToolExample) 2 | 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. 3 | 4 | ![Splash Image](docs/images/splash.png) 5 | 6 | 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. 7 | 8 | # Features 9 | These examples demonstrate how to implement the following: 10 | - adding modules 11 | - adding menus to the editor interface with: 12 | - sections 13 | - menu items 14 | - sub menus 15 | - adding custom widgets and controls (button and text input) to the menu 16 | - adding a dockable tab Window 17 | - custom details panel 18 | - defining and adding your own custom asset data type (data factory) 19 | - importing/reimporting custom data 20 | - custom editor mode 21 | - custom editor viewport widget 22 | - viewport object context sensitive right-click & menus 23 | - custom project settings/preferences 24 | - additional tricks and tips 25 | 26 | 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. 27 | 28 | 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. 29 | 30 | ## Branches 31 | - master 32 | - current development branch - this will likely have the most recent code - note: this branch does work with UE 5.2 Preview 1 33 | - 5.1 34 | - UE 5.1 Compatible 35 | - There was some header changes, a new module dependency, and class FEditorStyle was deprecated and replaced with FAppStyle. 36 | - I tested this very briefly, but it all looks good! 37 | - 4.26 38 | - UE 4.26 Compatible. 39 | - 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. 40 | - initial branch source is identical to 4.24/4.25 41 | - 4.25 42 | - UE 4.25 Compatible. 43 | - 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. 44 | - NOTE: The editor mode issue works in 4.26 without any code changes. 45 | - as it stands, this source is identical to the 4.24 branch 46 | - 4.24 47 | - UE 4.24 Compatible. 48 | - updates build.cs to V2 target. 49 | - implements [IWYU](https://docs.unrealengine.com/en-US/ProductionPipelines/BuildTools/UnrealBuildTool/IWYU/index.html) standard. 50 | - 4.23 51 | - original fork from [lxjk/ToolExample](https://github.com/lxjk/ToolExample). 52 | - UE 4.23 compatible. 53 | 54 | ## Contributing 55 | 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. 56 | 57 | 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. 58 | 59 | 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. 60 | 61 | ## System Requirements 62 | - Computer capable of Unreal Engine development 63 | - Epic Recommends [this.](https://docs.unrealengine.com/5.1/en-US/hardware-and-software-specifications-for-unreal-engine/) 64 | - The typical system used by developers at Epic Games looks like this: 65 | - Windows 10 64-bit (Version 20H2) 66 | - 64 GB RAM 67 | - 256 GB SSD (OS Drive) 68 | - 2 TB SSD (Data Drive) 69 | - NVIDIA GeForce RTX 2080 SUPER 70 | - Xoreax Incredibuild (Dev Tools Package) 71 | - Six-Core Xeon E5-2643 @ 3.4GHz 72 | - I get by with a 16 core AMD Ryzen 9, Windows 11, MSDev2022, and 40 GB RAM on my gaming laptop. 73 | - Unreal Engine 5.5 74 | - This repo also supports older versions, just download/clone the [branch](https://github.com/ScottKirvan/ToolExample/branches) you need. 75 | - Microsoft Visual Studio 2022 76 | - 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/). 77 | - This repo supports older versions, just clone the branch you need. 78 | - Microsoft Visual Studio 2019 79 | - 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`). 80 | 81 | ## Getting Started With Local Development 82 | 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. 83 | 84 | 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. 85 | 86 | A couple of other good resources for getting started: 87 | - [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/) 88 | - [UE5.1 Programming Quickstart](https://docs.unrealengine.com/5.1/en-US/unreal-engine-cpp-quick-start/) 89 | 90 | ## Support/Contact 91 | - 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. 92 | - 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. 93 | 94 | ## Submit a Feature Request 95 | Use the [Issues](https://github.com/ScottKirvan/ToolExample/issues) link, above. Thanks! 96 | 97 | ## Credits 98 | 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). 99 | 100 | Contributors: 101 | [Eric Zhang](https://github.com/lxjk) (OG!) 102 | [Scott Kirvan](https://github.com/ScottKirvan) (2021-present) 103 | [Razdvizh](https://github.com/Razdvizh) (2023) 104 | [duyaokun](https://github.com/duyaokun) (2024) 105 | You! (Future!) 106 | 107 | _ToolExample is licensed under the [MIT License](LICENSE.md)._ 108 | -------------------------------------------------------------------------------- /Source/ToolExample.Target.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using UnrealBuildTool; 4 | using System.Collections.Generic; 5 | 6 | public class ToolExampleTarget : TargetRules 7 | { 8 | public ToolExampleTarget(TargetInfo Target) : base(Target) 9 | { 10 | Type = TargetType.Game; 11 | 12 | ExtraModuleNames.AddRange( new string[] { "ToolExample" } ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/ToolExample/CustomDataType/ExampleData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ExampleData.generated.h" 4 | 5 | UCLASS(Blueprintable) 6 | class TOOLEXAMPLE_API UExampleData : public UObject 7 | { 8 | GENERATED_BODY() 9 | 10 | public: 11 | UPROPERTY(EditAnywhere, Category = "Properties") 12 | FString ExampleString; 13 | 14 | #if WITH_EDITORONLY_DATA 15 | UPROPERTY(Category = SourceAsset, VisibleAnywhere) 16 | FString SourceFilePath; 17 | #endif 18 | }; -------------------------------------------------------------------------------- /Source/ToolExample/DetailsCustomization/ExampleActor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ExampleActor.generated.h" 4 | 5 | UCLASS() 6 | class TOOLEXAMPLE_API AExampleActor : public AActor 7 | { 8 | GENERATED_BODY() 9 | 10 | public: 11 | 12 | UPROPERTY(EditAnywhere, Category = "Options") 13 | bool bOption1 = false; 14 | 15 | UPROPERTY(EditAnywhere, Category = "Options") 16 | bool bOption2 = false; 17 | 18 | UPROPERTY(EditAnywhere, Category = "Test") 19 | int testInt = 0; 20 | 21 | }; -------------------------------------------------------------------------------- /Source/ToolExample/EditorMode/ExampleTargetPoint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Engine/Targetpoint.h" 4 | #include "ExampleTargetPoint.generated.h" 5 | 6 | UCLASS() 7 | class TOOLEXAMPLE_API AExampleTargetPoint : public ATargetPoint 8 | { 9 | GENERATED_BODY() 10 | 11 | public: 12 | UPROPERTY(EditAnywhere, Category = "Points") 13 | TArray Points; 14 | }; -------------------------------------------------------------------------------- /Source/ToolExample/ToolExample.Build.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class ToolExample : ModuleRules 6 | { 7 | public ToolExample(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" }); 12 | 13 | PrivateDependencyModuleNames.AddRange(new string[] { }); 14 | 15 | // Uncomment if you are using Slate UI 16 | // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" }); 17 | 18 | // Uncomment if you are using online features 19 | // PrivateDependencyModuleNames.Add("OnlineSubsystem"); 20 | 21 | // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Source/ToolExample/ToolExample.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include "ToolExample.h" 4 | #include "Modules/ModuleManager.h" 5 | 6 | IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ToolExample, "ToolExample" ); 7 | -------------------------------------------------------------------------------- /Source/ToolExample/ToolExample.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | -------------------------------------------------------------------------------- /Source/ToolExample/ToolExampleGameModeBase.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "ToolExampleGameModeBase.h" 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Source/ToolExample/ToolExampleGameModeBase.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/GameModeBase.h" 7 | #include "ToolExampleGameModeBase.generated.h" 8 | 9 | /** 10 | * 11 | */ 12 | UCLASS() 13 | class TOOLEXAMPLE_API AToolExampleGameModeBase : public AGameModeBase 14 | { 15 | GENERATED_BODY() 16 | 17 | 18 | 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor.Target.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using UnrealBuildTool; 4 | using System.Collections.Generic; 5 | 6 | public class ToolExampleEditorTarget : TargetRules 7 | { 8 | public ToolExampleEditorTarget(TargetInfo Target) : base(Target) 9 | { 10 | Type = TargetType.Editor; 11 | DefaultBuildSettings = BuildSettingsVersion.V5; 12 | IncludeOrderVersion = EngineIncludeOrderVersion.Latest; 13 | 14 | ExtraModuleNames.AddRange( new string[] { "ToolExample" } ); 15 | ExtraModuleNames.AddRange( new string[] { "ToolExampleEditor" }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "ExampleDataFactory.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | #include "ToolExample/CustomDataType/ExampleData.h" 4 | 5 | UExampleDataFactory::UExampleDataFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) 6 | { 7 | SupportedClass = UExampleData::StaticClass(); 8 | bCreateNew = true; 9 | bEditAfterNew = true; 10 | } 11 | 12 | UObject* UExampleDataFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) 13 | { 14 | UExampleData* NewObjectAsset = NewObject(InParent, Class, Name, Flags | RF_Transactional); 15 | return NewObjectAsset; 16 | } -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/ExampleDataFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "UnrealEd.h" 4 | #include "ExampleDataFactory.generated.h" 5 | 6 | UCLASS() 7 | class UExampleDataFactory : public UFactory 8 | { 9 | GENERATED_UCLASS_BODY() 10 | public: 11 | // Begin UFactory Interface 12 | virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; 13 | // End UFactory Interface 14 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.cpp: -------------------------------------------------------------------------------- 1 | #include "ExampleDataTypeActions.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | #include "ToolExample/CustomDataType/ExampleData.h" 4 | #include "XMPAssetTypeActions_Base.h" 5 | 6 | 7 | FExampleDataTypeActions::FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory) 8 | : MyAssetCategory(InAssetCategory) 9 | { 10 | } 11 | 12 | FText FExampleDataTypeActions::GetName() const 13 | { 14 | return FText::FromString("Example Data"); 15 | } 16 | 17 | FColor FExampleDataTypeActions::GetTypeColor() const 18 | { 19 | return FColor(230, 205, 165); 20 | } 21 | 22 | UClass* FExampleDataTypeActions::GetSupportedClass() const 23 | { 24 | return UExampleData::StaticClass(); 25 | } 26 | 27 | uint32 FExampleDataTypeActions::GetCategories() 28 | { 29 | return MyAssetCategory; 30 | } 31 | 32 | void FExampleDataTypeActions::GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) 33 | { 34 | auto ExampleDataImports = GetTypedWeakObjectPtrs(InObjects); 35 | 36 | MenuBuilder.AddMenuEntry( 37 | FText::FromString("Reimport"), 38 | FText::FromString("Reimports example data."), 39 | FSlateIcon(), 40 | FUIAction( 41 | FExecuteAction::CreateSP(this, &FExampleDataTypeActions::ExecuteReimport, ExampleDataImports), 42 | FCanExecuteAction() 43 | ) 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/ExampleDataTypeActions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "XMPAssetTypeActions_Base.h" 4 | 5 | class UExampleData; 6 | 7 | class FExampleDataTypeActions : public FXMPAssetTypeActions_Base 8 | { 9 | public: 10 | FExampleDataTypeActions(EAssetTypeCategories::Type InAssetCategory); 11 | 12 | // IAssetTypeActions interface 13 | virtual FText GetName() const override; 14 | virtual FColor GetTypeColor() const override; 15 | virtual UClass* GetSupportedClass() const override; 16 | virtual uint32 GetCategories() override; 17 | virtual bool HasActions(const TArray& InObjects) const override { return true; } 18 | virtual void GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) override; 19 | // End of IAssetTypeActions interface 20 | 21 | private: 22 | EAssetTypeCategories::Type MyAssetCategory; 23 | }; 24 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/ReimportExampleDataFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "ReimportExampleDataFactory.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | #include "ExampleDataFactory.h" 4 | #include "ToolExample/CustomDataType/ExampleData.h" 5 | 6 | bool UReimportExampleDataFactory::CanReimport(UObject* Obj, TArray& OutFilenames) 7 | { 8 | UExampleData* ExampleData = Cast(Obj); 9 | if (ExampleData) 10 | { 11 | OutFilenames.Add(UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost())); 12 | return true; 13 | } 14 | return false; 15 | } 16 | 17 | void UReimportExampleDataFactory::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) 18 | { 19 | UExampleData* ExampleData = Cast(Obj); 20 | if (ExampleData && ensure(NewReimportPaths.Num() == 1)) 21 | { 22 | ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(NewReimportPaths[0], ExampleData->GetOutermost()); 23 | } 24 | } 25 | 26 | EReimportResult::Type UReimportExampleDataFactory::Reimport(UObject* Obj) 27 | { 28 | UExampleData* ExampleData = Cast(Obj); 29 | if (!ExampleData) 30 | { 31 | return EReimportResult::Failed; 32 | } 33 | 34 | const FString Filename = UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost()); 35 | if (!FPaths::GetExtension(Filename).Equals(TEXT("xmp"))) 36 | { 37 | return EReimportResult::Failed; 38 | } 39 | 40 | CurrentFilename = Filename; 41 | FString Data; 42 | if (FFileHelper::LoadFileToString(Data, *CurrentFilename)) 43 | { 44 | const TCHAR* Ptr = *Data; 45 | ExampleData->Modify(); 46 | ExampleData->MarkPackageDirty(); 47 | ExampleData->ExampleString = Ptr; 48 | // save the source file path and timestamp 49 | ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, ExampleData->GetOutermost()); 50 | } 51 | 52 | return EReimportResult::Succeeded; 53 | } 54 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/ReimportExampleDataFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ExampleDataFactory.h" 3 | #include "ReimportExampleDataFactory.generated.h" 4 | 5 | UCLASS() 6 | class UReimportExampleDataFactory : public UExampleDataFactory, public FReimportHandler 7 | { 8 | GENERATED_BODY() 9 | 10 | // Begin FReimportHandler interface 11 | virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override; 12 | virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override; 13 | virtual EReimportResult::Type Reimport(UObject* Obj) override; 14 | // End FReimportHandler interface 15 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "XMPAssetTypeActions_Base.h" 5 | #include "ToolExample/CustomDataType/ExampleData.h" 6 | #include "EditorReimportHandler.h" 7 | #include "EditorFramework/AssetImportData.h" 8 | #include "Misc/FileHelper.h" 9 | 10 | bool FXMPAssetTypeActions_Base::CanReimport(UObject* Obj, TArray& OutFilenames) 11 | { 12 | UExampleData* ExampleData = Cast(Obj); 13 | if (ExampleData) 14 | { 15 | OutFilenames.Add(UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost())); 16 | return true; 17 | } 18 | return false; 19 | } 20 | 21 | void FXMPAssetTypeActions_Base::SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) 22 | { 23 | check(NewReimportPaths.IsValidIndex(0)); 24 | if (UExampleData* ExampleData = Cast(Obj)) 25 | { 26 | ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(NewReimportPaths[0], ExampleData->GetOutermost()); 27 | } 28 | } 29 | 30 | EReimportResult::Type FXMPAssetTypeActions_Base::Reimport(UObject* Obj) 31 | { 32 | UExampleData* ExampleData = Cast(Obj); 33 | if (!ExampleData) 34 | { 35 | return EReimportResult::Failed; 36 | } 37 | 38 | const FString Filename = UAssetImportData::ResolveImportFilename(ExampleData->SourceFilePath, ExampleData->GetOutermost()); 39 | 40 | FString Data; 41 | if (FFileHelper::LoadFileToString(Data, *Filename)) 42 | { 43 | const TCHAR* Ptr = *Data; 44 | ExampleData->Modify(); 45 | ExampleData->MarkPackageDirty(); 46 | 47 | ExampleData->ExampleString = Ptr; 48 | ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(Filename, ExampleData->GetOutermost()); 49 | } 50 | 51 | return EReimportResult::Succeeded; 52 | } 53 | 54 | void FXMPAssetTypeActions_Base::ExecuteReimport(TArray> Objects) 55 | { 56 | for (auto ObjIt = Objects.CreateConstIterator(); ObjIt; ++ObjIt) 57 | { 58 | auto Object = (*ObjIt).Get(); 59 | if (Object) 60 | { 61 | Reimport(Object); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/XMPAssetTypeActions_Base.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "AssetTypeActions_Base.h" 7 | #include "EditorReimportHandler.h" 8 | 9 | class UExampleData; 10 | 11 | class FXMPAssetTypeActions_Base : public FAssetTypeActions_Base, public FReimportHandler 12 | { 13 | public: 14 | //It won't be shown in the content browser, it is a proxy for an `Example Data` asset 15 | virtual FColor GetTypeColor() const override { return FColor(0, 0, 0); } 16 | virtual bool HasActions(const TArray& InObjects) const override { return true; } 17 | virtual uint32 GetCategories() override { return EAssetTypeCategories::Misc; } 18 | virtual bool IsImportedAsset() const override { return true; } 19 | 20 | virtual bool CanReimport(UObject* Obj, TArray& OutFilenames) override; 21 | virtual void SetReimportPaths(UObject* Obj, const TArray& NewReimportPaths) override; 22 | virtual EReimportResult::Type Reimport(UObject* Obj) override; 23 | 24 | protected: 25 | void ExecuteReimport(TArray> Objects); 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/XMPImportFactory.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "XMPImportFactory.h" 5 | #include "ToolExample/CustomDataType/ExampleData.h" 6 | #include "EditorFramework/AssetImportData.h" 7 | 8 | #define LOCTEXT_NAMESPACE "XMPImportFactory" 9 | 10 | UXMPImportFactory::UXMPImportFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) 11 | { 12 | SupportedClass = UExampleData::StaticClass(); 13 | bCreateNew = false; 14 | bEditorImport = true; 15 | bEditAfterNew = true; 16 | bText = true; 17 | 18 | Formats.Add(TEXT("xmp;Example Data")); 19 | } 20 | 21 | FText UXMPImportFactory::GetDisplayName() const 22 | { 23 | return LOCTEXT("XMPImportFactoryDisplayName", "Example Data"); 24 | } 25 | 26 | 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) 27 | { 28 | GEditor->GetEditorSubsystem()->BroadcastAssetPreImport(this, InClass, InParent, InName, Type); 29 | 30 | UExampleData* ExampleData = CastChecked(NewObject(InParent, InName, Flags)); 31 | const int32 NumChars = BufferEnd - Buffer; 32 | ExampleData->ExampleString = FString(NumChars, Buffer); 33 | ExampleData->SourceFilePath = UAssetImportData::SanitizeImportFilename(CurrentFilename, ExampleData->GetOutermost()); 34 | 35 | GEditor->GetEditorSubsystem()->BroadcastAssetPostImport(this, ExampleData); 36 | 37 | return ExampleData; 38 | } 39 | 40 | bool UXMPImportFactory::FactoryCanImport(const FString& Filename) 41 | { 42 | return FPaths::GetExtension(Filename).Equals(TEXT("xmp")); 43 | } 44 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomDataType/XMPImportFactory.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Factories/Factory.h" 7 | #include "Factories/ImportSettings.h" 8 | #include "XMPImportFactory.generated.h" 9 | 10 | UCLASS(customconstructor) 11 | class UXMPImportFactory : public UFactory 12 | { 13 | GENERATED_UCLASS_BODY() 14 | 15 | public: 16 | UXMPImportFactory(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); 17 | 18 | virtual FText GetDisplayName() const override; 19 | 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; 20 | virtual bool FactoryCanImport(const FString& Filename) override; 21 | }; 22 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/CustomProjectSettings/ExampleSettings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ExampleSettings.generated.h" 4 | 5 | UCLASS(config = EditorUserSettings, defaultconfig) 6 | class UExampleSettings : public UObject 7 | { 8 | GENERATED_BODY() 9 | 10 | UPROPERTY(EditAnywhere, config, Category = Test) 11 | bool bTest = false; 12 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/DetailsCustomization/ExampleActorDetails.cpp: -------------------------------------------------------------------------------- 1 | #include "ExampleActorDetails.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | #include "ToolExample/DetailsCustomization/ExampleActor.h" 4 | 5 | //#include "DetailsCustomization/ExampleActor.h" 6 | 7 | TSharedRef FExampleActorDetails::MakeInstance() 8 | { 9 | return MakeShareable(new FExampleActorDetails); 10 | } 11 | 12 | void FExampleActorDetails::CustomizeDetails(IDetailLayoutBuilder& DetailLayout) 13 | { 14 | TArray> Objects; 15 | DetailLayout.GetObjectsBeingCustomized(Objects); 16 | if (Objects.Num() != 1) 17 | { 18 | // skip customization if select more than one objects 19 | return; 20 | } 21 | AExampleActor* actor = (AExampleActor*)Objects[0].Get(); 22 | 23 | // hide original property 24 | DetailLayout.HideProperty(DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(AExampleActor, bOption1))); 25 | DetailLayout.HideProperty(DetailLayout.GetProperty(GET_MEMBER_NAME_CHECKED(AExampleActor, bOption2))); 26 | 27 | // add custom widget to "Options" category 28 | IDetailCategoryBuilder& OptionsCategory = DetailLayout.EditCategory("Options", FText::FromString(""), ECategoryPriority::Important); 29 | OptionsCategory.AddCustomRow(FText::FromString("Options")) 30 | .WholeRowContent() 31 | [ 32 | SNew(SHorizontalBox) 33 | + SHorizontalBox::Slot() 34 | .AutoWidth() 35 | .VAlign(VAlign_Center) 36 | [ 37 | SNew(SCheckBox) 38 | .Style(FAppStyle::Get(), "RadioButton") 39 | .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 1) 40 | .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 1) 41 | [ 42 | SNew(STextBlock).Text(FText::FromString("Option 1")) 43 | ] 44 | ] 45 | + SHorizontalBox::Slot() 46 | .AutoWidth() 47 | .Padding(10.f, 0.f, 0.f, 0.f) 48 | .VAlign(VAlign_Center) 49 | [ 50 | SNew(SCheckBox) 51 | .Style(FAppStyle::Get(), "RadioButton") 52 | .IsChecked(this, &FExampleActorDetails::IsModeRadioChecked, actor, 2) 53 | .OnCheckStateChanged(this, &FExampleActorDetails::OnModeRadioChanged, actor, 2) 54 | [ 55 | SNew(STextBlock).Text(FText::FromString("Option 2")) 56 | ] 57 | ] 58 | ]; 59 | 60 | } 61 | 62 | ECheckBoxState FExampleActorDetails::IsModeRadioChecked(AExampleActor* actor, int optionIndex) const 63 | { 64 | bool bFlag = false; 65 | if (actor) 66 | { 67 | if (optionIndex == 1) 68 | bFlag = actor->bOption1; 69 | else if (optionIndex == 2) 70 | bFlag = actor->bOption2; 71 | } 72 | return bFlag ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; 73 | } 74 | 75 | void FExampleActorDetails::OnModeRadioChanged(ECheckBoxState CheckType, AExampleActor* actor, int optionIndex) 76 | { 77 | bool bFlag = (CheckType == ECheckBoxState::Checked); 78 | if (actor) 79 | { 80 | actor->Modify(); 81 | if (bFlag) 82 | { 83 | // clear all options first 84 | actor->bOption1 = false; 85 | actor->bOption2 = false; 86 | } 87 | if (optionIndex == 1) 88 | actor->bOption1 = bFlag; 89 | else if (optionIndex == 2) 90 | actor->bOption2 = bFlag; 91 | } 92 | } -------------------------------------------------------------------------------- /Source/ToolExampleEditor/DetailsCustomization/ExampleActorDetails.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IDetailCustomization.h" 4 | 5 | class AExampleActor; 6 | 7 | class FExampleActorDetails : public IDetailCustomization 8 | { 9 | public: 10 | /** Makes a new instance of this detail layout class for a specific detail view requesting it */ 11 | static TSharedRef MakeInstance(); 12 | 13 | /** IDetailCustomization interface */ 14 | virtual void CustomizeDetails(IDetailLayoutBuilder& DetailLayout) override; 15 | 16 | protected: 17 | 18 | // widget functions 19 | ECheckBoxState IsModeRadioChecked(AExampleActor* actor, int optionIndex) const; 20 | void OnModeRadioChanged(ECheckBoxState CheckType, AExampleActor* actor, int optionIndex); 21 | 22 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/EditorMode/ExampleEdMode.cpp: -------------------------------------------------------------------------------- 1 | #include "ExampleEdMode.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | #include "Toolkits/ToolkitManager.h" 4 | #include "ScopedTransaction.h" 5 | #include "ExampleEdModeToolkit.h" 6 | #include "ToolExample/EditorMode/ExampleTargetPoint.h" 7 | 8 | class ExampleEditorCommands : public TCommands 9 | { 10 | public: 11 | ExampleEditorCommands() : TCommands( 12 | "ExampleEditor", // Context name for fast lookup 13 | FText::FromString(TEXT("Example Editor")), // context name for displaying 14 | NAME_None, // Parent 15 | FAppStyle::GetAppStyleSetName()) 16 | { 17 | } 18 | 19 | #define LOCTEXT_NAMESPACE "" 20 | virtual void RegisterCommands() override 21 | { 22 | UI_COMMAND(DeletePoint, "Delete Point", "Delete the currently selected point.", EUserInterfaceActionType::Button, FInputGesture(EKeys::Delete)); 23 | } 24 | #undef LOCTEXT_NAMESPACE 25 | 26 | public: 27 | TSharedPtr DeletePoint; 28 | }; 29 | 30 | IMPLEMENT_HIT_PROXY(HExamplePointProxy, HHitProxy); 31 | 32 | const FEditorModeID FExampleEdMode::EM_Example(TEXT("EM_Example")); 33 | 34 | FExampleEdMode::FExampleEdMode() 35 | { 36 | ExampleEditorCommands::Register(); 37 | ExampleEdModeActions = MakeShareable(new FUICommandList); 38 | } 39 | 40 | FExampleEdMode::~FExampleEdMode() 41 | { 42 | ExampleEditorCommands::Unregister(); 43 | } 44 | 45 | void FExampleEdMode::MapCommands() 46 | { 47 | const auto &Commands = ExampleEditorCommands::Get(); 48 | 49 | ExampleEdModeActions->MapAction( 50 | Commands.DeletePoint, 51 | FExecuteAction::CreateSP(this, &FExampleEdMode::RemovePoint), 52 | FCanExecuteAction::CreateSP(this, &FExampleEdMode::CanRemovePoint)); 53 | } 54 | 55 | void FExampleEdMode::Enter() 56 | { 57 | FEdMode::Enter(); 58 | 59 | if (!Toolkit.IsValid()) 60 | { 61 | Toolkit = MakeShareable(new FExampleEdModeToolkit); 62 | Toolkit->Init(Owner->GetToolkitHost()); 63 | } 64 | 65 | // reset 66 | currentSelectedTarget = nullptr; 67 | currentSelectedIndex = -1; 68 | 69 | MapCommands(); 70 | } 71 | 72 | void FExampleEdMode::Exit() 73 | { 74 | FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef()); 75 | Toolkit.Reset(); 76 | 77 | FEdMode::Exit(); 78 | } 79 | 80 | void FExampleEdMode::Render(const FSceneView *View, FViewport *Viewport, FPrimitiveDrawInterface *PDI) 81 | { 82 | const FColor normalColor(200, 200, 200); 83 | const FColor selectedColor(255, 128, 0); 84 | 85 | UWorld *World = GetWorld(); 86 | for (TActorIterator It(World); It; ++It) 87 | { 88 | AExampleTargetPoint *actor = (*It); 89 | if (actor) 90 | { 91 | FVector actorLoc = actor->GetActorLocation(); 92 | for (int i = 0; i < actor->Points.Num(); ++i) 93 | { 94 | bool bSelected = (actor == currentSelectedTarget && i == currentSelectedIndex); 95 | const FColor &color = bSelected ? selectedColor : normalColor; 96 | // set hit proxy and draw 97 | PDI->SetHitProxy(new HExamplePointProxy(actor, i)); 98 | PDI->DrawPoint(actor->Points[i], color, 15.f, SDPG_Foreground); 99 | PDI->DrawLine(actor->Points[i], actorLoc, color, SDPG_Foreground); 100 | PDI->SetHitProxy(NULL); 101 | } 102 | } 103 | } 104 | 105 | FEdMode::Render(View, Viewport, PDI); 106 | } 107 | 108 | bool FExampleEdMode::HandleClick(FEditorViewportClient *InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) 109 | { 110 | bool isHandled = false; 111 | 112 | if (HitProxy) 113 | { 114 | if (HitProxy->IsA(HExamplePointProxy::StaticGetType())) 115 | { 116 | isHandled = true; 117 | HExamplePointProxy *examplePointProxy = (HExamplePointProxy *)HitProxy; 118 | AExampleTargetPoint *actor = Cast(examplePointProxy->RefObject); 119 | int32 index = examplePointProxy->Index; 120 | if (actor && index >= 0 && index < actor->Points.Num()) 121 | { 122 | SelectPoint(actor, index); 123 | } 124 | } 125 | } 126 | 127 | if (HitProxy && isHandled && Click.GetKey() == EKeys::RightMouseButton) 128 | { 129 | TSharedPtr MenuWidget = GenerateContextMenu(InViewportClient); 130 | if (MenuWidget.IsValid()) 131 | { 132 | FSlateApplication::Get().PushMenu( 133 | Owner->GetToolkitHost()->GetParentWidget(), 134 | FWidgetPath(), 135 | MenuWidget.ToSharedRef(), 136 | FSlateApplication::Get().GetCursorPos(), 137 | FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu)); 138 | } 139 | } 140 | 141 | return isHandled; 142 | } 143 | 144 | bool FExampleEdMode::InputDelta(FEditorViewportClient *InViewportClient, FViewport *InViewport, FVector &InDrag, FRotator &InRot, FVector &InScale) 145 | { 146 | if (InViewportClient->GetCurrentWidgetAxis() == EAxisList::None) 147 | { 148 | return false; 149 | } 150 | 151 | if (HasValidSelection()) 152 | { 153 | if (!InDrag.IsZero()) 154 | { 155 | currentSelectedTarget->Modify(); 156 | currentSelectedTarget->Points[currentSelectedIndex] += InDrag; 157 | } 158 | return true; 159 | } 160 | 161 | return false; 162 | } 163 | 164 | bool FExampleEdMode::InputKey(FEditorViewportClient *ViewportClient, FViewport *Viewport, FKey Key, EInputEvent Event) 165 | { 166 | bool isHandled = false; 167 | 168 | if (!isHandled && Event == IE_Pressed) 169 | { 170 | isHandled = ExampleEdModeActions->ProcessCommandBindings(Key, FSlateApplication::Get().GetModifierKeys(), false); 171 | } 172 | 173 | return isHandled; 174 | } 175 | 176 | TSharedPtr FExampleEdMode::GenerateContextMenu(FEditorViewportClient *InViewportClient) const 177 | { 178 | FMenuBuilder MenuBuilder(true, NULL); 179 | 180 | MenuBuilder.PushCommandList(ExampleEdModeActions.ToSharedRef()); 181 | MenuBuilder.BeginSection("Example Section"); 182 | if (HasValidSelection()) 183 | { 184 | // add label for point index 185 | TSharedRef LabelWidget = 186 | SNew(STextBlock) 187 | .Text(FText::FromString(FString::FromInt(currentSelectedIndex))) 188 | .ColorAndOpacity(FLinearColor::Green); 189 | MenuBuilder.AddWidget(LabelWidget, FText::FromString(TEXT("Point Index: "))); 190 | MenuBuilder.AddMenuSeparator(); 191 | // add delete point entry 192 | MenuBuilder.AddMenuEntry(ExampleEditorCommands::Get().DeletePoint); 193 | } 194 | MenuBuilder.EndSection(); 195 | MenuBuilder.PopCommandList(); 196 | 197 | TSharedPtr MenuWidget = MenuBuilder.MakeWidget(); 198 | return MenuWidget; 199 | } 200 | 201 | bool FExampleEdMode::ShowModeWidgets() const 202 | { 203 | return true; 204 | } 205 | 206 | bool FExampleEdMode::ShouldDrawWidget() const 207 | { 208 | return true; 209 | } 210 | 211 | bool FExampleEdMode::UsesTransformWidget() const 212 | { 213 | return true; 214 | } 215 | 216 | FVector FExampleEdMode::GetWidgetLocation() const 217 | { 218 | if (HasValidSelection()) 219 | { 220 | return currentSelectedTarget->Points[currentSelectedIndex]; 221 | } 222 | return FEdMode::GetWidgetLocation(); 223 | } 224 | 225 | AExampleTargetPoint *GetSelectedTargetPointActor() 226 | { 227 | TArray selectedObjects; 228 | GEditor->GetSelectedActors()->GetSelectedObjects(selectedObjects); 229 | if (selectedObjects.Num() == 1) 230 | { 231 | return Cast(selectedObjects[0]); 232 | } 233 | return nullptr; 234 | } 235 | 236 | void FExampleEdMode::AddPoint() 237 | { 238 | AExampleTargetPoint *actor = GetSelectedTargetPointActor(); 239 | if (actor) 240 | { 241 | const FScopedTransaction Transaction(FText::FromString("Add Point")); 242 | 243 | // add new point, slightly in front of camera 244 | FEditorViewportClient *client = (FEditorViewportClient *)GEditor->GetActiveViewport()->GetClient(); 245 | FVector newPoint = client->GetViewLocation() + client->GetViewRotation().Vector() * 50.f; 246 | actor->Modify(); 247 | actor->Points.Add(newPoint); 248 | // auto select this new point 249 | SelectPoint(actor, actor->Points.Num() - 1); 250 | } 251 | } 252 | 253 | bool FExampleEdMode::CanAddPoint() const 254 | { 255 | return GetSelectedTargetPointActor() != nullptr; 256 | } 257 | 258 | void FExampleEdMode::RemovePoint() 259 | { 260 | if (HasValidSelection()) 261 | { 262 | const FScopedTransaction Transaction(FText::FromString("Remove Point")); 263 | 264 | currentSelectedTarget->Modify(); 265 | currentSelectedTarget->Points.RemoveAt(currentSelectedIndex); 266 | // deselect the point 267 | SelectPoint(nullptr, -1); 268 | } 269 | } 270 | 271 | bool FExampleEdMode::CanRemovePoint() const 272 | { 273 | return HasValidSelection(); 274 | } 275 | 276 | bool FExampleEdMode::HasValidSelection() const 277 | { 278 | return currentSelectedTarget.IsValid() && currentSelectedIndex >= 0 && currentSelectedIndex < currentSelectedTarget->Points.Num(); 279 | } 280 | 281 | void FExampleEdMode::SelectPoint(AExampleTargetPoint *actor, int32 index) 282 | { 283 | currentSelectedTarget = actor; 284 | currentSelectedIndex = index; 285 | 286 | // select this actor only 287 | if (currentSelectedTarget.IsValid()) 288 | { 289 | GEditor->SelectNone(true, true); 290 | GEditor->SelectActor(currentSelectedTarget.Get(), true, true); 291 | } 292 | } -------------------------------------------------------------------------------- /Source/ToolExampleEditor/EditorMode/ExampleEdMode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "EditorModes.h" 4 | #include "EdMode.h" 5 | 6 | struct HExamplePointProxy : public HHitProxy 7 | { 8 | DECLARE_HIT_PROXY(); 9 | 10 | HExamplePointProxy(UObject* InRefObject, int32 InIndex) 11 | : HHitProxy(HPP_UI), RefObject(InRefObject), Index(InIndex) 12 | {} 13 | 14 | UObject* RefObject; 15 | int32 Index; 16 | }; 17 | 18 | class AExampleTargetPoint; 19 | 20 | class FExampleEdMode : public FEdMode 21 | { 22 | public: 23 | 24 | const static FEditorModeID EM_Example; 25 | 26 | // FEdMode interface 27 | virtual void Enter() override; 28 | virtual void Exit() override; 29 | virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override; 30 | //virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override; 31 | virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy *HitProxy, const FViewportClick &Click) override; 32 | virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override; 33 | virtual bool InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) override; 34 | virtual bool ShowModeWidgets() const override; 35 | virtual bool ShouldDrawWidget() const override; 36 | virtual bool UsesTransformWidget() const override; 37 | virtual FVector GetWidgetLocation() const override; 38 | //virtual bool GetCustomDrawingCoordinateSystem(FMatrix& InMatrix, void* InData) override; 39 | //virtual bool GetCustomInputCoordinateSystem(FMatrix& InMatrix, void* InData) override; 40 | //virtual void ActorSelectionChangeNotify() override; 41 | //virtual void MapChangeNotify() override; 42 | //virtual void SelectionChanged() override; 43 | //virtual bool IsCompatibleWith(FEditorModeID OtherModeID) const override; 44 | // End of FEdMode interface 45 | 46 | FExampleEdMode(); 47 | ~FExampleEdMode(); 48 | 49 | void AddPoint(); 50 | bool CanAddPoint() const; 51 | void RemovePoint(); 52 | bool CanRemovePoint() const; 53 | bool HasValidSelection() const; 54 | void SelectPoint(AExampleTargetPoint* actor, int32 index); 55 | 56 | TWeakObjectPtr currentSelectedTarget; 57 | int32 currentSelectedIndex = -1; 58 | 59 | 60 | TSharedPtr ExampleEdModeActions; 61 | void MapCommands(); 62 | TSharedPtr GenerateContextMenu(FEditorViewportClient* InViewportClient) const; 63 | 64 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/EditorMode/ExampleEdModeTool.cpp: -------------------------------------------------------------------------------- 1 | #include "ExampleEdModeTool.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | #include "ExampleEdMode.h" 4 | 5 | #define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__) 6 | 7 | TSharedPtr< FSlateStyleSet > ExampleEdModeTool::StyleSet = nullptr; 8 | 9 | void ExampleEdModeTool::OnStartupModule() 10 | { 11 | RegisterStyleSet(); 12 | RegisterEditorMode(); 13 | } 14 | 15 | void ExampleEdModeTool::OnShutdownModule() 16 | { 17 | UnregisterStyleSet(); 18 | UnregisterEditorMode(); 19 | } 20 | 21 | void ExampleEdModeTool::RegisterStyleSet() 22 | { 23 | // Const icon sizes 24 | const FVector2D Icon20x20(20.0f, 20.0f); 25 | const FVector2D Icon40x40(40.0f, 40.0f); 26 | 27 | // Only register once 28 | if (StyleSet.IsValid()) 29 | { 30 | return; 31 | } 32 | 33 | StyleSet = MakeShareable(new FSlateStyleSet("ExampleEdModeToolStyle")); 34 | StyleSet->SetContentRoot(FPaths::ProjectDir() / TEXT("Content/EditorResources")); 35 | StyleSet->SetCoreContentRoot(FPaths::ProjectDir() / TEXT("Content/EditorResources")); 36 | 37 | // Spline editor 38 | { 39 | StyleSet->Set("ExampleEdMode", new IMAGE_BRUSH(TEXT("IconExampleEditorMode"), Icon40x40)); 40 | StyleSet->Set("ExampleEdMode.Small", new IMAGE_BRUSH(TEXT("IconExampleEditorMode"), Icon20x20)); 41 | } 42 | 43 | FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get()); 44 | } 45 | 46 | void ExampleEdModeTool::UnregisterStyleSet() 47 | { 48 | if (StyleSet.IsValid()) 49 | { 50 | FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get()); 51 | ensure(StyleSet.IsUnique()); 52 | StyleSet.Reset(); 53 | } 54 | } 55 | 56 | void ExampleEdModeTool::RegisterEditorMode() 57 | { 58 | FEditorModeRegistry::Get().RegisterMode( 59 | FExampleEdMode::EM_Example, 60 | FText::FromString("Example Editor Mode"), 61 | FSlateIcon(StyleSet->GetStyleSetName(), "ExampleEdMode", "ExampleEdMode.Small"), 62 | true, 500 63 | ); 64 | } 65 | 66 | void ExampleEdModeTool::UnregisterEditorMode() 67 | { 68 | FEditorModeRegistry::Get().UnregisterMode(FExampleEdMode::EM_Example); 69 | } 70 | 71 | #undef IMAGE_BRUSH -------------------------------------------------------------------------------- /Source/ToolExampleEditor/EditorMode/ExampleEdModeTool.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ToolExampleEditor/ExampleTabToolBase.h" 4 | 5 | class ExampleEdModeTool : public FExampleTabToolBase 6 | { 7 | public: 8 | virtual void OnStartupModule() override; 9 | virtual void OnShutdownModule() override; 10 | 11 | virtual ~ExampleEdModeTool() {} 12 | private: 13 | static TSharedPtr< class FSlateStyleSet > StyleSet; 14 | 15 | void RegisterStyleSet(); 16 | void UnregisterStyleSet(); 17 | 18 | void RegisterEditorMode(); 19 | void UnregisterEditorMode(); 20 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/EditorMode/ExampleEdModeToolkit.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Toolkits/BaseToolkit.h" 4 | #include "ExampleEdMode.h" 5 | #include "SExampleEdModeWidget.h" 6 | 7 | class FExampleEdModeToolkit: public FModeToolkit 8 | { 9 | public: 10 | 11 | FExampleEdModeToolkit() 12 | { 13 | SAssignNew(ExampleEdModeWidget, SExampleEdModeWidget); 14 | } 15 | 16 | /** IToolkit interface */ 17 | virtual FName GetToolkitFName() const override { return FName("ExampleEdMode"); } 18 | virtual FText GetBaseToolkitName() const override { return NSLOCTEXT("BuilderModeToolkit", "DisplayName", "Builder"); } 19 | virtual class FEdMode* GetEditorMode() const override { return GLevelEditorModeTools().GetActiveMode(FExampleEdMode::EM_Example); } 20 | virtual TSharedPtr GetInlineContent() const override { return ExampleEdModeWidget; } 21 | 22 | private: 23 | 24 | TSharedPtr ExampleEdModeWidget; 25 | }; 26 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.cpp: -------------------------------------------------------------------------------- 1 | #include "SExampleEdModeWidget.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | #include "ExampleEdMode.h" 4 | 5 | void SExampleEdModeWidget::Construct(const FArguments& InArgs) 6 | { 7 | ChildSlot 8 | [ 9 | SNew(SScrollBox) 10 | + SScrollBox::Slot() 11 | .VAlign(VAlign_Top) 12 | .Padding(5.f) 13 | [ 14 | SNew(SVerticalBox) 15 | + SVerticalBox::Slot() 16 | .AutoHeight() 17 | .Padding(0.f, 5.f, 0.f, 0.f) 18 | [ 19 | SNew(STextBlock) 20 | .Text(FText::FromString(TEXT("This is a editor mode example."))) 21 | ] 22 | + SVerticalBox::Slot() 23 | .AutoHeight() 24 | .Padding(0.f, 5.f, 0.f, 0.f) 25 | [ 26 | SNew(SHorizontalBox) 27 | + SHorizontalBox::Slot() 28 | .AutoWidth() 29 | .Padding(2, 0, 0, 0) 30 | .VAlign(VAlign_Center) 31 | [ 32 | SNew(SButton) 33 | .Text(FText::FromString("Add")) 34 | .OnClicked(this, &SExampleEdModeWidget::OnAddPoint) 35 | .IsEnabled(this, &SExampleEdModeWidget::CanAddPoint) 36 | ] 37 | + SHorizontalBox::Slot() 38 | .AutoWidth() 39 | .VAlign(VAlign_Center) 40 | .Padding(0, 0, 2, 0) 41 | [ 42 | SNew(SButton) 43 | .Text(FText::FromString("Remove")) 44 | .OnClicked(this, &SExampleEdModeWidget::OnRemovePoint) 45 | .IsEnabled(this, &SExampleEdModeWidget::CanRemovePoint) 46 | ] 47 | ] 48 | ] 49 | ]; 50 | } 51 | 52 | FExampleEdMode* SExampleEdModeWidget::GetEdMode() const 53 | { 54 | return (FExampleEdMode*)GLevelEditorModeTools().GetActiveMode(FExampleEdMode::EM_Example); 55 | } 56 | 57 | FReply SExampleEdModeWidget::OnAddPoint() 58 | { 59 | GetEdMode()->AddPoint(); 60 | return FReply::Handled(); 61 | } 62 | 63 | bool SExampleEdModeWidget::CanAddPoint() const 64 | { 65 | return GetEdMode()->CanAddPoint(); 66 | } 67 | 68 | FReply SExampleEdModeWidget::OnRemovePoint() 69 | { 70 | GetEdMode()->RemovePoint(); 71 | return FReply::Handled(); 72 | } 73 | 74 | bool SExampleEdModeWidget::CanRemovePoint() const 75 | { 76 | return GetEdMode()->CanRemovePoint(); 77 | } -------------------------------------------------------------------------------- /Source/ToolExampleEditor/EditorMode/SExampleEdModeWidget.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Framework/APplication/SlateApplication.h" 4 | 5 | class SExampleEdModeWidget : public SCompoundWidget 6 | { 7 | public: 8 | SLATE_BEGIN_ARGS(SExampleEdModeWidget) {} 9 | SLATE_END_ARGS(); 10 | 11 | void Construct(const FArguments& InArgs); 12 | 13 | // Util Functions 14 | class FExampleEdMode* GetEdMode() const; 15 | 16 | FReply OnAddPoint(); 17 | bool CanAddPoint() const; 18 | FReply OnRemovePoint(); 19 | bool CanRemovePoint() const; 20 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/ExampleTabToolBase.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ToolExampleEditor/ToolExampleEditor.h" 4 | #include "ToolExampleEditor/IExampleModuleInterface.h" 5 | #include "Framework/Docking/TabManager.h" 6 | #include "Widgets/Docking/SDockTab.h" 7 | 8 | class FExampleTabToolBase : public IExampleModuleListenerInterface, public TSharedFromThis< FExampleTabToolBase > 9 | { 10 | 11 | public: 12 | 13 | // IPixelopusToolBase 14 | virtual void OnStartupModule() override 15 | { 16 | Initialize(); 17 | FGlobalTabmanager::Get()->RegisterNomadTabSpawner(TabName, FOnSpawnTab::CreateRaw(this, &FExampleTabToolBase::SpawnTab)) 18 | .SetGroup(FToolExampleEditor::Get().GetMenuRoot()) 19 | .SetDisplayName(TabDisplayName) 20 | .SetTooltipText(ToolTipText); 21 | }; 22 | 23 | virtual void OnShutdownModule() override 24 | { 25 | FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(TabName); 26 | }; 27 | 28 | // In this function set TabName/TabDisplayName/ToolTipText 29 | virtual void Initialize() {}; 30 | virtual TSharedRef SpawnTab(const FSpawnTabArgs& TabSpawnArgs) { return SNew(SDockTab); }; 31 | 32 | virtual void MakeMenuEntry(FMenuBuilder &menuBuilder) 33 | { 34 | FGlobalTabmanager::Get()->PopulateTabSpawnerMenu(menuBuilder, TabName); 35 | }; 36 | 37 | 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 38 | protected: 39 | FName TabName; 40 | FText TabDisplayName; 41 | FText ToolTipText; 42 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/IExampleModuleInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Modules/ModuleManager.h" 4 | 5 | 6 | class IExampleModuleListenerInterface 7 | { 8 | public: 9 | virtual void OnStartupModule() {}; 10 | virtual void OnShutdownModule() {}; 11 | }; 12 | 13 | class IExampleModuleInterface : public IModuleInterface 14 | { 15 | public: 16 | void StartupModule() override 17 | { 18 | if (!IsRunningCommandlet()) 19 | { 20 | AddModuleListeners(); 21 | for (int32 i = 0; i < ModuleListeners.Num(); ++i) 22 | { 23 | ModuleListeners[i]->OnStartupModule(); 24 | } 25 | } 26 | } 27 | 28 | void ShutdownModule() override 29 | { 30 | for (int32 i = 0; i < ModuleListeners.Num(); ++i) 31 | { 32 | ModuleListeners[i]->OnShutdownModule(); 33 | } 34 | } 35 | 36 | virtual void AddModuleListeners() {}; 37 | 38 | protected: 39 | TArray> ModuleListeners; 40 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/MenuTool/MenuTool.cpp: -------------------------------------------------------------------------------- 1 | #include "MenuTool.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | 4 | #include "ScopedTransaction.h" 5 | 6 | #define LOCTEXT_NAMESPACE "MenuTool" 7 | 8 | class MenuToolCommands : public TCommands 9 | { 10 | public: 11 | MenuToolCommands() 12 | : TCommands( 13 | TEXT("MenuTool"), // Context name for fast lookup 14 | FText::FromString("Example Menu tool"), // Context name for displaying 15 | NAME_None, // No parent context 16 | FAppStyle::GetAppStyleSetName() // Icon Style Set 17 | ) 18 | { 19 | } 20 | 21 | virtual void RegisterCommands() override 22 | { 23 | UI_COMMAND(MenuCommand1, "Menu Command 1", "Test Menu Command 1.", EUserInterfaceActionType::Button, FInputGesture()); 24 | UI_COMMAND(MenuCommand2, "Menu Command 2", "Test Menu Command 2.", EUserInterfaceActionType::Button, FInputGesture()); 25 | UI_COMMAND(MenuCommand3, "Menu Command 3", "Test Menu Command 3.", EUserInterfaceActionType::Button, FInputGesture()); 26 | } 27 | 28 | public: 29 | TSharedPtr MenuCommand1; 30 | TSharedPtr MenuCommand2; 31 | TSharedPtr MenuCommand3; 32 | }; 33 | 34 | void MenuTool::MapCommands() 35 | { 36 | const auto &Commands = MenuToolCommands::Get(); 37 | 38 | CommandList->MapAction( 39 | Commands.MenuCommand1, 40 | FExecuteAction::CreateSP(this, &MenuTool::MenuCommand1), 41 | FCanExecuteAction()); 42 | 43 | CommandList->MapAction( 44 | Commands.MenuCommand2, 45 | FExecuteAction::CreateSP(this, &MenuTool::MenuCommand2), 46 | FCanExecuteAction()); 47 | 48 | CommandList->MapAction( 49 | Commands.MenuCommand3, 50 | FExecuteAction::CreateSP(this, &MenuTool::MenuCommand3), 51 | FCanExecuteAction()); 52 | } 53 | 54 | void MenuTool::OnStartupModule() 55 | { 56 | CommandList = MakeShareable(new FUICommandList); 57 | MenuToolCommands::Register(); 58 | MapCommands(); 59 | FToolExampleEditor::Get().AddMenuExtension( 60 | FMenuExtensionDelegate::CreateRaw(this, &MenuTool::MakeMenuEntry), 61 | FName("Section_1"), 62 | CommandList); 63 | } 64 | 65 | void MenuTool::OnShutdownModule() 66 | { 67 | MenuToolCommands::Unregister(); 68 | } 69 | 70 | void MenuTool::MakeMenuEntry(FMenuBuilder &menuBuilder) 71 | { 72 | menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand1); 73 | menuBuilder.AddSubMenu( 74 | FText::FromString("Sub Menu"), 75 | FText::FromString("This is example sub menu"), 76 | FNewMenuDelegate::CreateSP(this, &MenuTool::MakeSubMenu)); 77 | 78 | // add tag 79 | TSharedRef AddTagWidget = 80 | 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)]; 81 | 82 | menuBuilder.AddWidget(AddTagWidget, FText::FromString("")); 83 | } 84 | 85 | void MenuTool::MakeSubMenu(FMenuBuilder &menuBuilder) 86 | { 87 | menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand2); 88 | menuBuilder.AddMenuEntry(MenuToolCommands::Get().MenuCommand3); 89 | } 90 | 91 | void MenuTool::MenuCommand1() 92 | { 93 | UE_LOG(LogClass, Log, TEXT("clicked MenuCommand1")); 94 | } 95 | 96 | void MenuTool::MenuCommand2() 97 | { 98 | UE_LOG(LogClass, Log, TEXT("clicked MenuCommand2")); 99 | } 100 | 101 | void MenuTool::MenuCommand3() 102 | { 103 | UE_LOG(LogClass, Log, TEXT("clicked MenuCommand3")); 104 | } 105 | 106 | FReply MenuTool::AddTag() 107 | { 108 | if (!TagToAdd.IsNone()) 109 | { 110 | const FScopedTransaction Transaction(FText::FromString("Add Tag")); 111 | for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) 112 | { 113 | AActor *Actor = static_cast(*It); 114 | if (!Actor->Tags.Contains(TagToAdd)) 115 | { 116 | Actor->Modify(); 117 | Actor->Tags.Add(TagToAdd); 118 | } 119 | } 120 | } 121 | 122 | return FReply::Handled(); 123 | } 124 | 125 | FText MenuTool::GetTagToAddText() const 126 | { 127 | return FText::FromName(TagToAdd); 128 | } 129 | 130 | void MenuTool::OnTagToAddTextCommited(const FText &InText, ETextCommit::Type CommitInfo) 131 | { 132 | FString str = InText.ToString(); 133 | TagToAdd = FName(*str.TrimStart()); 134 | } 135 | 136 | #undef LOCTEXT_NAMESPACE -------------------------------------------------------------------------------- /Source/ToolExampleEditor/MenuTool/MenuTool.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ToolExampleEditor/IExampleModuleInterface.h" 4 | 5 | class MenuTool : public IExampleModuleListenerInterface, public TSharedFromThis 6 | { 7 | public: 8 | virtual ~MenuTool() {} 9 | 10 | virtual void OnStartupModule() override; 11 | virtual void OnShutdownModule() override; 12 | 13 | void MakeMenuEntry(FMenuBuilder &menuBuilder); 14 | void MakeSubMenu(FMenuBuilder &menuBuilder); 15 | 16 | 17 | protected: 18 | TSharedPtr CommandList; 19 | 20 | void MapCommands(); 21 | 22 | //************************ 23 | // UI Command functions 24 | void MenuCommand1(); 25 | void MenuCommand2(); 26 | void MenuCommand3(); 27 | 28 | FName TagToAdd; 29 | 30 | FReply AddTag(); 31 | FText GetTagToAddText() const; 32 | void OnTagToAddTextCommited(const FText& InText, ETextCommit::Type CommitInfo); 33 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/TabTool/TabTool.cpp: -------------------------------------------------------------------------------- 1 | #include "TabTool.h" 2 | //#include "AssetRegistryModule.h" 3 | //#include "ScopedTransaction.h" 4 | //#include "SDockTab.h" 5 | //#include "SDockableTab.h" 6 | //#include "SDockTabStack.h" 7 | //#include "SlateApplication.h" 8 | #include "ToolExampleEditor/ToolExampleEditor.h" 9 | #include "TabToolPanel.h" 10 | 11 | void TabTool::OnStartupModule() 12 | { 13 | FExampleTabToolBase::OnStartupModule(); 14 | FToolExampleEditor::Get().AddMenuExtension(FMenuExtensionDelegate::CreateRaw(this, &TabTool::MakeMenuEntry), FName("Section_2")); 15 | } 16 | 17 | void TabTool::OnShutdownModule() 18 | { 19 | FExampleTabToolBase::OnShutdownModule(); 20 | } 21 | 22 | void TabTool::Initialize() 23 | { 24 | TabName = "TabTool"; 25 | TabDisplayName = FText::FromString("Tab Tool"); 26 | ToolTipText = FText::FromString("Tab Tool Window"); 27 | } 28 | 29 | TSharedRef TabTool::SpawnTab(const FSpawnTabArgs& TabSpawnArgs) 30 | { 31 | TSharedRef SpawnedTab = SNew(SDockTab) 32 | .TabRole(ETabRole::NomadTab) 33 | [ 34 | SNew(TabToolPanel) 35 | .Tool(SharedThis(this)) 36 | ]; 37 | 38 | return SpawnedTab; 39 | } -------------------------------------------------------------------------------- /Source/ToolExampleEditor/TabTool/TabTool.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ToolExampleEditor/ExampleTabToolBase.h" 4 | 5 | class TabTool : public FExampleTabToolBase 6 | { 7 | public: 8 | virtual ~TabTool() {} 9 | virtual void OnStartupModule() override; 10 | virtual void OnShutdownModule() override; 11 | virtual void Initialize() override; 12 | virtual TSharedRef SpawnTab(const FSpawnTabArgs& TabSpawnArgs) override; 13 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/TabTool/TabToolPanel.cpp: -------------------------------------------------------------------------------- 1 | #include "TabToolPanel.h" 2 | #include "ToolExampleEditor/ToolExampleEditor.h" 3 | 4 | void TabToolPanel::Construct(const FArguments& InArgs) 5 | { 6 | tool = InArgs._Tool; 7 | 8 | if (tool.IsValid()) 9 | { 10 | // do anything you need from tool object 11 | } 12 | 13 | ChildSlot 14 | [ 15 | SNew(SScrollBox) 16 | + SScrollBox::Slot() 17 | .VAlign(VAlign_Top) 18 | .Padding(5) 19 | [ 20 | SNew(SBorder) 21 | .BorderBackgroundColor(FColor(192, 192, 192, 255)) 22 | .Padding(15.0f) 23 | [ 24 | SNew(STextBlock) 25 | .Text(FText::FromString(TEXT("This is a tab example."))) 26 | ] 27 | ] 28 | ]; 29 | } -------------------------------------------------------------------------------- /Source/ToolExampleEditor/TabTool/TabToolPanel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Widgets/Docking/SDockTab.h" 4 | #include "Framework/Application/SlateApplication.h" 5 | #include "TabTool.h" 6 | 7 | class TabToolPanel : public SCompoundWidget 8 | { 9 | 10 | SLATE_BEGIN_ARGS(TabToolPanel) 11 | { 12 | } 13 | 14 | SLATE_ARGUMENT(TWeakPtr, Tool) 15 | SLATE_END_ARGS() 16 | 17 | void Construct(const FArguments &InArgs); 18 | 19 | protected: 20 | TWeakPtr tool; 21 | }; -------------------------------------------------------------------------------- /Source/ToolExampleEditor/ToolExampleEditor.Build.cs: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | using UnrealBuildTool; 4 | using System.IO; 5 | 6 | public class ToolExampleEditor : ModuleRules 7 | { 8 | public ToolExampleEditor(ReadOnlyTargetRules Target) : base(Target) 9 | { 10 | 11 | //PrivatePCHHeaderFile = "ToolExampleEditor.h"; // TODO - see if adding this back in helps much with normal dev cycles 12 | 13 | PublicIncludePaths.AddRange( 14 | new string[] { 15 | // ... add public include paths required here ... 16 | } 17 | ); 18 | 19 | PrivateIncludePaths.AddRange( 20 | new string[] { 21 | // ... add other private include paths required here ... 22 | } 23 | ); 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] { 27 | "Core", 28 | "Engine", 29 | "CoreUObject", 30 | "InputCore", 31 | "LevelEditor", 32 | "Slate", 33 | "EditorStyle", 34 | "AssetTools", 35 | "EditorWidgets", 36 | "UnrealEd", 37 | "BlueprintGraph", 38 | "AnimGraph", 39 | "ComponentVisualizers", 40 | "ToolExample" 41 | } 42 | ); 43 | 44 | PrivateDependencyModuleNames.AddRange( 45 | new string[] 46 | { 47 | "Core", 48 | "CoreUObject", 49 | "Engine", 50 | "AppFramework", 51 | "SlateCore", 52 | "EditorFramework", 53 | "AnimGraph", 54 | "UnrealEd", 55 | "KismetWidgets", 56 | "MainFrame", 57 | "PropertyEditor", 58 | "ComponentVisualizers", 59 | "ToolExample" 60 | // ... add private dependencies that you statically link with here ... 61 | } 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/ToolExampleEditor.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #include "ToolExampleEditor.h" 4 | #include "MenuTool/MenuTool.h" 5 | #include "TabTool/TabTool.h" 6 | #include "EditorMode/ExampleEdModeTool.h" 7 | 8 | #include "ToolExample/DetailsCustomization/ExampleActor.h" 9 | #include "ToolExampleEditor/DetailsCustomization/ExampleActorDetails.h" 10 | 11 | #include "CustomDataType/ExampleDataTypeActions.h" 12 | 13 | #include "ISettingsModule.h" 14 | #include "Developer/Settings/Public/ISettingsContainer.h" 15 | #include "CustomProjectSettings/ExampleSettings.h" 16 | 17 | IMPLEMENT_GAME_MODULE(FToolExampleEditor, ToolExampleEditor) 18 | 19 | TSharedRef FToolExampleEditor::MenuRoot = FWorkspaceItem::NewGroup(FText::FromString("Menu Root")); 20 | 21 | void FToolExampleEditor::AddModuleListeners() 22 | { 23 | // add tools 24 | ModuleListeners.Add(MakeShareable(new MenuTool)); 25 | ModuleListeners.Add(MakeShareable(new TabTool)); 26 | ModuleListeners.Add(MakeShareable(new ExampleEdModeTool)); 27 | } 28 | 29 | void FToolExampleEditor::StartupModule() 30 | { 31 | if (!IsRunningCommandlet()) 32 | { 33 | FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); 34 | LevelEditorMenuExtensibilityManager = LevelEditorModule.GetMenuExtensibilityManager(); 35 | MenuExtender = MakeShareable(new FExtender); 36 | MenuExtender->AddMenuBarExtension("Window", EExtensionHook::After, NULL, FMenuBarExtensionDelegate::CreateRaw(this, &FToolExampleEditor::MakePulldownMenu)); 37 | LevelEditorMenuExtensibilityManager->AddExtender(MenuExtender); 38 | } 39 | 40 | // register custom layouts 41 | { 42 | static FName PropertyEditor("PropertyEditor"); 43 | FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked(PropertyEditor); 44 | PropertyModule.RegisterCustomClassLayout(AExampleActor::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FExampleActorDetails::MakeInstance)); 45 | } 46 | 47 | // register custom types: 48 | { 49 | IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); 50 | // add custom category 51 | EAssetTypeCategories::Type ExampleCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Example")), FText::FromString("Example")); 52 | // register our custom asset with example category 53 | TSharedPtr Action = MakeShareable(new FExampleDataTypeActions(ExampleCategory)); 54 | AssetTools.RegisterAssetTypeActions(Action.ToSharedRef()); 55 | // saved it here for unregister later 56 | CreatedAssetTypeActions.Add(Action); 57 | } 58 | 59 | // register settings: 60 | { 61 | ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings"); 62 | if (SettingsModule) 63 | { 64 | TSharedPtr ProjectSettingsContainer = SettingsModule->GetContainer("Project"); 65 | ProjectSettingsContainer->DescribeCategory("ExampleCategory", FText::FromString("Example Category"), FText::FromString("Example settings description text here")); 66 | 67 | SettingsModule->RegisterSettings("Project", "ExampleCategory", "ExampleSettings", 68 | FText::FromString("Example Settings"), 69 | FText::FromString("Configure Example Settings"), 70 | GetMutableDefault() 71 | ); 72 | } 73 | } 74 | 75 | IExampleModuleInterface::StartupModule(); 76 | } 77 | 78 | void FToolExampleEditor::ShutdownModule() 79 | { 80 | // unregister custom layouts 81 | if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) 82 | { 83 | FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked("PropertyEditor"); 84 | PropertyModule.UnregisterCustomClassLayout(AExampleActor::StaticClass()->GetFName()); 85 | } 86 | 87 | // Unregister all the asset types that we registered 88 | if (FModuleManager::Get().IsModuleLoaded("AssetTools")) 89 | { 90 | IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); 91 | for (int32 i = 0; i < CreatedAssetTypeActions.Num(); ++i) 92 | { 93 | AssetTools.UnregisterAssetTypeActions(CreatedAssetTypeActions[i].ToSharedRef()); 94 | } 95 | } 96 | CreatedAssetTypeActions.Empty(); 97 | 98 | // unregister settings 99 | ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings"); 100 | if (SettingsModule) 101 | { 102 | SettingsModule->UnregisterSettings("Project", "ExampleCategory", "ExampleSettings"); 103 | } 104 | 105 | IExampleModuleInterface::ShutdownModule(); 106 | } 107 | 108 | 109 | void FToolExampleEditor::AddMenuExtension(const FMenuExtensionDelegate &extensionDelegate, FName extensionHook, const TSharedPtr &CommandList, EExtensionHook::Position position) 110 | { 111 | MenuExtender->AddMenuExtension(extensionHook, position, CommandList, extensionDelegate); 112 | } 113 | 114 | void FToolExampleEditor::MakePulldownMenu(FMenuBarBuilder &menuBuilder) 115 | { 116 | menuBuilder.AddPullDownMenu( 117 | FText::FromString("Example"), 118 | FText::FromString("Open the Example menu"), 119 | FNewMenuDelegate::CreateRaw(this, &FToolExampleEditor::FillPulldownMenu), 120 | "Example", 121 | FName(TEXT("ExampleMenu")) 122 | ); 123 | } 124 | 125 | void FToolExampleEditor::FillPulldownMenu(FMenuBuilder &menuBuilder) 126 | { 127 | menuBuilder.BeginSection("ExampleSection", FText::FromString("Section 1")); 128 | menuBuilder.AddMenuSeparator(FName("Section_1")); 129 | menuBuilder.EndSection(); 130 | 131 | menuBuilder.BeginSection("ExampleSection", FText::FromString("Section 2")); 132 | menuBuilder.AddMenuSeparator(FName("Section_2")); 133 | menuBuilder.EndSection(); 134 | } 135 | -------------------------------------------------------------------------------- /Source/ToolExampleEditor/ToolExampleEditor.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "UnrealEd.h" 6 | //#include "Engine.h" 7 | #include "SlateBasics.h" 8 | #include "SlateExtras.h" 9 | //#include "Internationalization.h" 10 | //#include "MultiBoxExtender.h" 11 | #include "Editor/LevelEditor/Public/LevelEditor.h" 12 | #include "Editor/PropertyEditor/Public/PropertyEditing.h" 13 | #include "IAssetTypeActions.h" 14 | 15 | #include "IExampleModuleInterface.h" 16 | 17 | class FToolExampleEditor : public IExampleModuleInterface 18 | { 19 | public: 20 | /** IModuleInterface implementation */ 21 | virtual void StartupModule() override; 22 | virtual void ShutdownModule() override; 23 | 24 | virtual void AddModuleListeners() override; 25 | 26 | /** 27 | * Singleton-like access to this module's interface. This is just for convenience! 28 | * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. 29 | * 30 | * @return Returns singleton instance, loading the module on demand if needed 31 | */ 32 | static inline FToolExampleEditor& Get() 33 | { 34 | return FModuleManager::LoadModuleChecked< FToolExampleEditor >("ToolExampleEditor"); 35 | } 36 | 37 | /** 38 | * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. 39 | * 40 | * @return True if the module is loaded and ready to use 41 | */ 42 | static inline bool IsAvailable() 43 | { 44 | return FModuleManager::Get().IsModuleLoaded("ToolExampleEditor"); 45 | } 46 | 47 | void AddMenuExtension(const FMenuExtensionDelegate &extensionDelegate, FName extensionHook, const TSharedPtr &CommandList = NULL, EExtensionHook::Position position = EExtensionHook::Before); 48 | TSharedRef GetMenuRoot() { return MenuRoot; }; 49 | 50 | protected: 51 | TSharedPtr LevelEditorMenuExtensibilityManager; 52 | TSharedPtr MenuExtender; 53 | 54 | static TSharedRef MenuRoot; 55 | 56 | TArray> CreatedAssetTypeActions; 57 | 58 | void MakePulldownMenu(FMenuBarBuilder &menuBuilder); 59 | void FillPulldownMenu(FMenuBuilder &menuBuilder); 60 | }; -------------------------------------------------------------------------------- /ToolExample.uproject: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "EngineAssociation": "5.5", 4 | "Category": "", 5 | "Description": "", 6 | "Modules": [ 7 | { 8 | "Name": "ToolExample", 9 | "Type": "Runtime", 10 | "LoadingPhase": "Default" 11 | }, 12 | { 13 | "Name": "ToolExampleEditor", 14 | "Type": "Editor", 15 | "LoadingPhase": "PostEngineInit", 16 | "AdditionalDependencies": [ 17 | "Engine", 18 | "UnrealEd" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /docs/images/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/001.png -------------------------------------------------------------------------------- /docs/images/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/002.png -------------------------------------------------------------------------------- /docs/images/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/003.png -------------------------------------------------------------------------------- /docs/images/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/004.png -------------------------------------------------------------------------------- /docs/images/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/005.png -------------------------------------------------------------------------------- /docs/images/006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/006.png -------------------------------------------------------------------------------- /docs/images/007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/007.png -------------------------------------------------------------------------------- /docs/images/008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/008.png -------------------------------------------------------------------------------- /docs/images/009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/009.png -------------------------------------------------------------------------------- /docs/images/010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/010.png -------------------------------------------------------------------------------- /docs/images/011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/011.png -------------------------------------------------------------------------------- /docs/images/012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/012.png -------------------------------------------------------------------------------- /docs/images/013.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/013.png -------------------------------------------------------------------------------- /docs/images/014.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/014.png -------------------------------------------------------------------------------- /docs/images/015.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/015.png -------------------------------------------------------------------------------- /docs/images/016.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/016.png -------------------------------------------------------------------------------- /docs/images/017.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/017.png -------------------------------------------------------------------------------- /docs/images/018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/018.png -------------------------------------------------------------------------------- /docs/images/019.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/019.png -------------------------------------------------------------------------------- /docs/images/020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/020.png -------------------------------------------------------------------------------- /docs/images/021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/021.png -------------------------------------------------------------------------------- /docs/images/022.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/022.png -------------------------------------------------------------------------------- /docs/images/023.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/023.png -------------------------------------------------------------------------------- /docs/images/024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/024.png -------------------------------------------------------------------------------- /docs/images/025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/025.png -------------------------------------------------------------------------------- /docs/images/026.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/026.png -------------------------------------------------------------------------------- /docs/images/027.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/027.png -------------------------------------------------------------------------------- /docs/images/028.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/028.png -------------------------------------------------------------------------------- /docs/images/029.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/029.png -------------------------------------------------------------------------------- /docs/images/030.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/030.png -------------------------------------------------------------------------------- /docs/images/031.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/031.png -------------------------------------------------------------------------------- /docs/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScottKirvan/ToolExample/d183639ef53e1ce7c4bc78bca57d4ef77e2dd889/docs/images/splash.png --------------------------------------------------------------------------------