Recover built-in "Create Packed Level Actor" button logic through Python API or Blueprint

(UE 5.3.)

Hello,

I am trying to do a button which simply reparents a newly created Packed Level Actor after its creation to another custom Packed Level Actor Blueprint in order to give it some additional logic (which is not too important here).

Ideally, this button would retake the whole logic of the Create Packed Level Actor button (which you can find in Actor Options/Level/ when selecting an actor inside a level) and then recover the newly created PLA to reparent it.

I have a few pictures here to show how the current button works and the PLA creation button.

My first question is:

Can you access the logic of a built-in button (which logic is set in CPP) through the Python API or a Blueprint?

This way, I would reference the functions used by the Create Packed Level Actor button in my script/BP, catch the PLA and reparent it, all in one button.

A second issue I might have is that the menus accessible from the content browser by right clicking on a PLA BP are not contextual to PLA but to general Blueprint Class, meaning the button I set for now would be accessible for any Blueprint Class, not only PLA.

Is there a way for these menus to be contextual to the parent class (here, PLA) instead of generic Blueprint Class?

Note that I cannot use a CPP Project, I have to stick to BP.

Best regards,

Thibault Lacharme

Steps to Reproduce

Hi Thibault,

Sorry about the delay. Let’s go over your questions.

Can you access the logic of a built-in button (which logic is set in CPP) through the Python API or a Blueprint?

Unfortunately, without C++, it is not possible to replicate the behavior of the “Create Packed Level Actor” menu item action, because many of the functions required to implement this feature are not exported to BP/Python (and this is true even up to UE 5.6). On the other hand, if C++ was a possibility, you could export the required functions yourself or call them from C++ directly. The menu item is added by this call chain:

FLevelInstanceEditorModule::StartupModule() FLevelInstanceEditorModule::ExtendContextMenu() LevelInstanceMenuUtils::CreateCreateMenu()Clicking on it triggers another call chain which includes the following:

LevelInstanceMenuUtils::CreateLevelInstanceFromSelection() ULevelInstanceSubsystem::CreateLevelInstanceFrom() EditorLevelUtils::CreateNewStreamingLevelForWorld() FPackedLevelActorBuilder::CreatePackedLevelActorBlueprintWithDialog() FPackedLevelActorBuilder::CreatePackedLevelActorBlueprint()In particular, ULevelInstanceSubsystem::CreateLevelInstanceFrom() receives a “LevelInstanceClass” parameter that is set to APackedLevelActor::StaticClass() by default, but you could call it with a different base class if needed.

Is there a way for these menus to be contextual to the parent class (here, PLA) instead of generic Blueprint Class?

You can do this using only blueprints by creating an “Editor Utility Blueprint” asset and selecting “Asset Action Utility” as the parent class. Then, override function “IsActionForBlueprints” to return true, and in the “Class Defaults”, add “PackedLevelActor” to the “Supported Classes” array. This should make a new menu item appear in the Content Browser’s right-click menu only for blueprints derived from the chosen parent class. However, that menu item will not be added to the “Blueprint Class Actions” category of the context menu, but rather as a sub-item under “Common - Scripted Asset Actions”. Let me know if this is acceptable for you.

If that “Create PLA Station” menu item must be placed on the “Blueprint Class Actions” section of the Content Browser’s context menu, I may need a bit more information about how that was implemented so far to be able to help you further, because I am not aware of a method to achieve that without C++. If that particular menu extension was done in C++, at some point the code probably has access to an array of FAssetData of the selected assets. It is possible to call GetAsset() on them and cast to UBlueprint, then call IsChildOf() on the BP’s generated class to check if they are of the desired type. If you have difficulty with this, or if this was done with blueprints only, could you kindly share some more details about how it was done? Then I can try to help you further from there.

Best regards,

Vitor

Hello Vitor,

No problem about the delay, thank you for the comprehensive answer.

Can you access the logic of a built-in button (which logic is set in CPP) through the Python API or a Blueprint?

That is what I thought but I needed to be sure, unfortunately we are bound to use Blueprint for specific reasons but it is good to know how we could do such a thing in C++ if we get the possibility to switch at some point.

Is there a way for these menus to be contextual to the parent class (here, PLA) instead of generic Blueprint Class?

The first part of your answer here is how we do it in general for calling blueprint scripts but it leads to a few more manipulations that we wanted to avoid since it seems we can add new buttons to existing menus (like what was shown in the pictures of my first message).

Ideally, I would want to add said button to one of the existing categories e.g. Blueprint Class Actions, Common, etc. or even my own sub category that I would add to the menu.

On top of that, I would want said button to be available only when I am right clicking on a Packed Level Actor but it seems that with the current implementation and design in Unreal, this menu is contextual to blueprint objects in general and does not distinguish between parent classes (this menu would appear for a Static Mesh Actor, a Skeletal Mesh Actor, etc. any object of “Blueprint Class”).

For now I decided to add the button to the LevelEditor.ActorContextMenu.LevelSubMenu (i.e. the sub menu where you go to create a Packed Level Actor from an object selected inside an open level from the viewport, you can see that on the first picture below) but it is not conditioned to appear only when selecting Packed Level Actor.

Here is what the python code looks like:

`import unreal

@unreal.uclass()

class PLAStationMenu(unreal.ToolMenuEntryScript):

“”"

Class for the button used to create a PLA Station

(i.e. reparent the selected PLA to the BP PLAStation)

It implements all the logic to create a button inside an

existing menu, add a custom section and call on a function

from a specific Blueprint.

“”"

state = unreal.uproperty(bool, getter=‘get_state’, setter=‘set_state’)

def init(self):

“”"

Function to initiate the object PLAStationMenu:

  • Initiates its parent class object (ToolMenuEntryScript).

  • Calls on init_menu to initiate the button.

Args:

self (PLAStationMenu): the object itself.

Returns:

No returns.

“”"

super().init()

self.init_menu()

@unreal.ufunction(override=True)

def execute(self, context):

“”"

Function to load the specific BP’s function we want the button.

to call. It is an override of the existing function for ToolMenuEntryScript.

Args:

self (PLAStationMenu): the object itself.

context

“”"

print(f’[PLA] Executing button’)

TODO set the asset path correctly

PLA_factory = unreal.EditorAssetLibrary.load_asset(“/Game/Developers/thibaultlacharme/PLA/EU_PLA”)

bp_class = unreal.load_object(None, PLA_factory.generated_class().get_path_name())

bp_cdo = unreal.get_default_object(bp_class)

bp_cdo.call_method(“CreatePLAStation”)

def init_menu(self):

“”"

Function to initiate the button in the menu and adding the custom

category PLA Preset.

Args:

self (PLAStationMenu): the object itself.

“”"

owning_menu_name = ‘LevelEditor.ActorContextMenu.LevelSubMenu’

category_name = ‘PLA Preset’

self.data.menu = owning_menu_name

self.data.advanced.entry_type = unreal.MultiBlockType.MENU_ENTRY

self.data.advanced.user_interface_action_type = unreal.UserInterfaceActionType.BUTTON

self.data.icon = unreal.ScriptSlateIcon(“EditorStyle”, “MergeActors.MeshMergingTool”)

self.init_entry(owner_name=‘editorUtilities’,

menu=‘’, section=category_name,

name=‘Create PLA Station’,

label=‘Create PLA Station’,

tool_tip=‘Convert PLA to PLA Station’)

tools_menus:unreal.ToolMenus = unreal.ToolMenus.get()

menu = tools_menus.extend_menu(owning_menu_name)

Add section “PLA Station” to the menu

menu.add_section(section_name=category_name, label=‘PLA Preset’)

Add our button to the menu

menu.add_menu_entry_object(self)

tools_menus.refresh_all_widgets()

if name == ‘main’:

PLAStationMenu()`Could I condition the creation of said button depending on the object I selected to open the menu on? It seems I could override the function Construct Menu Entry in Blueprint, a function which is called every time I open the sub menu.

While it seems I might not be able to do so in Blueprint (the function override gets run but I cannot use Add Menu Entry Object on the fly in Blueprint, only in the Run event read once at project launch), could I maybe do a check right before the self.init_menu() to get the current object’s parent class and, if it is a Packed Level Actor, call on the init function?

If such a check is possible, I might be able to do an operation similar to the picture with the Execute Python Script (i.e. using get_class to recover the class and, if it is a Packed Level Actor class, go through the init logic).

Sorry for the delay of my answer, thanks again for your first input!

Best regards,

Thibault

[Image Removed][Image Removed]

Hello Vitor,

I just tested out your solution and it works as intended!

So I’d override the is_visible function if I only want a certain button to be contextual or I’d use a dynamic section if I want a whole section to be contextual.

Thanks for the complete answer, you can switch this topic’s status to answered.

Best regards,

Thibault

Hi Thibault Lacharme, sorry about the slow turnaround on this one. I needed to pass this on to a partner, and I’m not sure about how readily they can take it. If you have urgency, I can try to expedite a follow up here asap. Otherwise, if you can wait a bit longer, I should be able to give you an answer by next monday. Do you want me to try to expedite an answer?

Hi Thibault Lacharme,

Thank you for your patience. Sorry about the slow turnaround on this one.

To control whether a specific entry appears in a context menu, you can override its is_visible() function from the base class unreal.ToolMenuEntryScript, similarly to how you did with the execute() function:

`@unreal.uclass()
class MyEntry(unreal.ToolMenuEntryScript):

def init(self):
super().init()
self.data.section = ‘MySection’
self.data.name = ‘MyEntry’
self.data.label = ‘My Entry’
self.data.tool_tip = ‘Do something useful’
self.data.icon = unreal.ScriptSlateIcon(“EditorStyle”, “MergeActors.MeshMergingTool”)

@unreal.ufunction(override=True)
def execute(self, context:unreal.ToolMenuContext):
unreal.log(“MyEntry.execute()”)

@unreal.ufunction(override=True)
def is_visible(self, context:unreal.ToolMenuContext):

Check if the context actor is a PackedLevelActor

context_actor = get_context_actor(context)
return isinstance(context_actor, unreal.PackedLevelActor)`If you’d like to control the visibility of multiple menu entries or entire sections/subcategories at once, you can use unreal.ToolMenu.add_dynamic_section() instead of unreal.ToolMenu.add_section(). This function takes as argument an object created from a subclass of unreal.ToolMenuSectionDynamic, where you can override function construct_sections() to dynamically add entries and sections based on any desired logic:

`@unreal.uclass()
class MyDynamicSection(unreal.ToolMenuSectionDynamic):

@unreal.ufunction(override=True)
def construct_sections(self, menu:unreal.ToolMenu, context:unreal.ToolMenuContext):

Check if the context actor is a PackedLevelActor

context_actor = get_context_actor(context)
if isinstance(context_actor, unreal.PackedLevelActor):
menu.add_section(‘MySection’, ‘My Section’)
menu.add_menu_entry_object(MyEntry())

def extend_menus():

tool_menus = unreal.ToolMenus.get()
tool_menu = tool_menus.extend_menu(‘LevelEditor.ActorContextMenu.LevelSubMenu’)
tool_menu.add_dynamic_section(‘MyDynamicSection’, MyDynamicSection())`Finally, to get the context actor on a right-click menu, you must use function unreal.ToolMenus.find_context() to get a sub-context of type unreal.LevelEditorContextMenuContext, then access its “hit_proxy_actor” property. Alternatively, if the menu was not generated by a right-click on an actor in the Level Editor, you can access the currently selected actors and work with one/some/all of them:

`def get_context_actor(context:unreal.ToolMenuContext) → unreal.Actor:

Try to get from the right-click menu context

tool_menus = unreal.ToolMenus.get()
level_editor_context = tool_menus.find_context(context, unreal.LevelEditorContextMenuContext)
if level_editor_context is not None:
if level_editor_context.hit_proxy_actor is not None:
unreal.log(‘Got from Right-Click menu context’)
return level_editor_context.hit_proxy_actor

Try to get from the selection (when using the menu bar or scene outliner)

eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
selected_actors = eas.get_selected_level_actors()
if len(selected_actors) > 0:
if selected_actors[0] is not None:
unreal.log(‘Got from selection’)
return selected_actors[0]

No context actor was found

return None`Note that, besides “LevelEditor.ActorContextMenu.LevelSubMenu”, you might also want to extend “LevelEditor.LevelEditorSceneOutliner.ContextMenu.LevelSubMenu”, which is the menu that appears when right-clicking an actor on the Scene Outliner.

Here’s the complete Python code from my tests:

`def get_context_actor(context:unreal.ToolMenuContext) → unreal.Actor:

Try to get from the right-click menu context

tool_menus = unreal.ToolMenus.get()
level_editor_context = tool_menus.find_context(context, unreal.LevelEditorContextMenuContext)
if level_editor_context is not None:
if level_editor_context.hit_proxy_actor is not None:
unreal.log(‘Got from Right-Click menu context’)
return level_editor_context.hit_proxy_actor

Try to get from the selection (when using the menu bar or scene outliner)

eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
selected_actors = eas.get_selected_level_actors()
if len(selected_actors) > 0:
if selected_actors[0] is not None:
unreal.log(‘Got from selection’)
return selected_actors[0]

No context actor was found

return None

@unreal.uclass()
class PLAEntryCreate(unreal.ToolMenuEntryScript):

def init(self):
super().init()
self.data.section = ‘PLAPresetSection’
self.data.name = ‘PLAEntryCreate’
self.data.label = ‘Create PLA Station’
self.data.tool_tip = ‘Convert PLA to PLA Station’
self.data.icon = unreal.ScriptSlateIcon(“EditorStyle”, “MergeActors.MeshMergingTool”)

@unreal.ufunction(override=True)
def execute(self, context:unreal.ToolMenuContext):
unreal.log(“PLAEntryCreate.execute()”)

@unreal.ufunction(override=True)
def is_visible(self, context:unreal.ToolMenuContext):

Check if the context actor is a PackedLevelActor

context_actor = get_context_actor(context)
return isinstance(context_actor, unreal.PackedLevelActor)

@unreal.uclass()
class PLADynamicSection(unreal.ToolMenuSectionDynamic):

@unreal.ufunction(override=True)
def construct_sections(self, menu:unreal.ToolMenu, context:unreal.ToolMenuContext):

Check if the context actor is a PackedLevelActor

context_actor = get_context_actor(context)
if isinstance(context_actor, unreal.PackedLevelActor):
menu.add_section(‘PLAPresetSection’, ‘PLA Preset’)
menu.add_menu_entry_object(PLAEntryCreate())

def add_dynamic_section(menu_name):

tool_menus = unreal.ToolMenus.get()
tool_menu = tool_menus.extend_menu(menu_name)
tool_menu.add_dynamic_section(‘PLADynamicSection’, PLADynamicSection())

def extend_menus():

add_dynamic_section(‘LevelEditor.ActorContextMenu.LevelSubMenu’)
add_dynamic_section(‘LevelEditor.LevelEditorSceneOutliner.ContextMenu.LevelSubMenu’)

#tool_menus.refresh_all_widgets()`Let me know if this works!

Best regards,

Vitor

Hello Vitor, thanks for the time you took to come back with an exhaustive answer!

I will try what you just told me when I get the time to do so to see if it could work for us (I will come back to you then).

PS : I didn’t get the notification where you asked about the urgency, it wasn’t that urgent so no problem on that, thanks for keeping me updated though.