Adding menu entry to content browser asset menu?

I want to inject my own menu entry into the UAsset menu, which is shown when right clicking any UAsset inside the editor. This menu entry must be shown for all UObjects.

I have tried to register new actions for a UObject using:

FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools");
AssetToolsModule.Get().RegisterAssetTypeActions(MakeShareable(new FAssetTypeActions_Object));
class FAssetTypeActions_Object : public FAssetTypeActions_Base {

public:

	// IAssetTypeActions Implementation
	virtual FText GetName() const override { return NSLOCTEXT("Test", "Test", "Test"); }
	virtual FColor GetTypeColor() const override { return FColor(0,232,0); }
	virtual UClass* GetSupportedClass() const override { return UObject::StaticClass(); }
	virtual uint32 GetCategories() override { return EAssetTypeCategories::None; }
	virtual void GetActions(const TArray<UObject*>& InObjects, struct FToolMenuSection& Section) override;
	// End IAssetTypeActions
};

But none of this will show up in the editor. How is this done?

bump

Attempt B, another failure.

FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
TArray<FContentBrowserMenuExtender_SelectedAssets>& MenuExtenders = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
// Inject a menu entry on the asset menu every time one is opened by right clicking an asset.
MenuExtenders.Add(FContentBrowserMenuExtender_SelectedAssets::CreateLambda([&](const TArray<FAssetData>& InAssetData) { 
    TSharedRef<FExtender> MenuExtender = MakeShareable(new FExtender);

      TArray<TWeakObjectPtr<UObject>> InSupportedObjects;
      for (FAssetData AssetDataX : InAssetData) {
          if (!IsObjectExcluded(AssetDataX.GetAsset())) {
              InSupportedObjects.Add(AssetDataX.GetAsset());
          }
      }

      if (InSupportedObjects.Num() == 0) {
          return MenuExtender;
      }

      MenuExtender->AddMenuExtension(
          "Asset",
          EExtensionHook::After,
          TSharedPtr<FUICommandList>(),
          FMenuExtensionDelegate::CreateLambda([&, this, InSupportedObjects] (FMenuBuilder& MenuBuilder) {
              MenuBuilder.BeginSection("NewAssetSection", NSLOCTEXT("NewAssetMenu", "Title"));
              MenuBuilder.AddMenuEntry(
                  NSLOCTEXT("NewAssetMenu", "Title"),
                  NSLOCTEXT("NewAssetMenu", "ToolTip"),
                  FSlateIcon(),
                  FUIAction(FExecuteAction::CreateLambda([&, this, InSupportedObjects](){
                      SomeClass::DoStuff(InSupportedObjects);
                  }))
              );
              MenuBuilder.EndSection();
          })
      );
      return MenuExtender; 
  }));

All goes well up to this point

SomeClass::DoStuff(InSupportedObjects);

Which is never reached. No errors no nothing

Try calling a method rather than using a lambda:

FUIAction(FExecuteAction::CreateSP(this,&FContentBrowserMenuExtension::ContextMenuItem_MyOption_Selected))

Here’s a whole routine that uses a class:

// h

class FContentBrowserMenuExtension : public TSharedFromThis<FContentBrowserMenuExtension> {
public:
			FContentBrowserMenuExtension(const TArray<FAssetData,FDefaultAllocator> assets);

	void	AddMenuEntry(FMenuBuilder& MenuBuilder);

	void	FillMySubmenu(FMenuBuilder& MenuBuilder);

	void	ContextMenuItem_MyOption_Selected();
};


// c++

void FContentBrowserMenuExtension::AddMenuEntry(FMenuBuilder& MenuBuilder) {

	MenuBuilder.BeginSection(NAME_None,LOCTEXT("myContextMenu1","My Menu"));	{

		// Create our Menu
		MenuBuilder.AddSubMenu(	FText::FromString("My Menu"),
								FText::FromString("My Menu"),
								FNewMenuDelegate::CreateSP(this,&FContentBrowserMenuExtension::FillMySubmenu),
								false,
								FSlateIcon()
								);
	}
	MenuBuilder.EndSection();
}

void FContentBrowserMenuExtension::FillMySubmenu(FMenuBuilder& MenuBuilder) {

	// Create the Submenu Entries
	MenuBuilder.AddMenuEntry(FText::FromString("My Option"),FText::FromString("MyOption"),FSlateIcon(),FUIAction(FExecuteAction::CreateSP(this,&FContentBrowserMenuExtension::ContextMenuItem_MyOption_Selected)));

}

FContentBrowserMenuExtension::FContentBrowserMenuExtension(const TArray<FAssetData,FDefaultAllocator> assets) {

	for(auto asset:assets) if(asset.AssetClass==TEXT("StaticMesh")) selectedMeshes.Add(asset);

}

void FContentBrowserMenuExtension::ContextMenuItem_MyOption_Selected() { 
 // do my thang...
}



TSharedRef<FExtender> handleMenu(const TArray<FAssetData>& assets) { // you know what I mean...

	TSharedPtr<FExtender> MenuExtender=MakeShareable(new FExtender());

  	TSharedPtr<FContentBrowserMenuExtension> menuExtension=MakeShareable(new FContentBrowserMenuExtension(assets));

	// Create a Shared-pointer delegate that keeps a weak reference to object
	MenuExtender->AddMenuExtension("CommonAssetActions",EExtensionHook::After,TSharedPtr<FUICommandList>(),
														FMenuExtensionDelegate::CreateSP(menuExtension.ToSharedRef(), 
														&FContentBrowserMenuExtension::AddMenuEntry));

	return MenuExtender.ToSharedRef();
}

Thanks, I got it working. I don’t know why it wasn’t working using lambdas as I was doing something very similar to Blutility but it works in the new format.

1 Like

I figured out what the problem was, it was not the lambda.

The part that says “Asset” had to be changed to “CommonAssetActions” to make the menu entry show up. You can not write any name here as the editor will just skip the entry entirely and silently.

1 Like

For someone looking for more details, there are several things that can go wrong when implementing a menu extender.

First, as @Roy_Wierer.Seda145 mentioned, you can’t make up an arbitrary name for your extender’s ExtensionHook parameter. It has to be predefined by unreal (or probably another extension that runs before you, but I didn’t verify this). For instance, “Asset” won’t work but “CommonAssetActions” will since the latter is predefined by unreal.

You can figure out what those predefine names are by enabling the engine’s developer tool. See this thread.

Second, if you use CreateSP to creating the delegate for AddMenuExtension, you have to remember that CreateSP creates a weak reference to the user object (the suffix SP is confusing as it leads people to think it actually creates a strong shared pointer), and thus when the shared pointer to your extender implementation class goes out of scope, the delegate will become unbound, leading to the extension callback to not be called at all.

An example in this post solves this by storing the shared pointer inside the module instance itself. See the following lines from their example:

// Extension variable contains a copy of selected paths array, you must keep Extension somewhere to prevent it from being deleted/garbage collected!
Extension = MakeShareable(new FContentBrowserMenuExtension(Path));

But I think the better way is to just use a lambda and capture the shared pointer by copy. Here is my approach:

TSharedRef<FExtender> YourModule::CreateExtender(const TArray<FAssetData>& InAssets)
{
	TSharedPtr<FYourExtenderImpl> ExtenderImpl = MakeShareable(new FYourExtenderImpl(InAssets));
	TSharedPtr<FExtender> Extender = MakeShareable(new FExtender);

	Extender->AddMenuExtension(
		"CommonAssetActions",
		EExtensionHook::After,
		{},
		FMenuExtensionDelegate::CreateLambda([ExtenderImpl, this](auto&& ... Args)
		{
			ExtenderImpl->AddMenuEntry(Forward<decltype(Args)>(Args)...);
		}));
	
	return Extender.ToSharedRef();
}