Unreal 扩展 内容浏览器 编辑器 快捷键 命令 ContentBrowser Editor Extender Command Shortcut Key

需求:快速定位Unreal资源在Window文件管理器中的位置

拆解需求

  • 在Unreal内容浏览器中选中资源或者文件夹右键菜单中 “在浏览器中显示”已经此功能

  • 为了更快速可以添加快捷键方式

  • 直接修改源码发现修改的文件有点多,所以选择扩展命令方式实现

遇到的问题

  • 快捷键不响应

    • 文件类型菜单和资源类型菜单是2种 分别直接添加命令 快捷键不响应

      void FMyAssetToolsModule::RegisterMenus()
      {
      	{
      		UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("ContentBrowser.FolderContextMenu");
      		{
      			FToolMenuSection& Section = Menu->FindOrAddSection("PathViewFolderOptions");
      			AddExploreFolderCommand(Section);
      		}
      	}
      
      	{
      		UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("ContentBrowser.AssetContextMenu");
      		{
      			FToolMenuSection& Section = Menu->FindOrAddSection("AssetContextExploreMenuOptions");
      			AddExploreFolderCommand(Section);
      		}
      	}
      }
      
      void FMyAssetToolsModule::AddExploreFolderCommand(FToolMenuSection& Section) const
      {
      	FFormatNamedArguments Args;
      	Args.Add(TEXT("FileManagerName"), FPlatformMisc::GetFileManagerName());
      	const FText LabelOverride = FText::Format(NSLOCTEXT("GenericPlatform", "ShowInFileManager", "Show in {FileManagerName}"), Args);
      
      	Section.AddMenuEntryWithCommandList(FMyAssetToolsCommands::Get().ExploreFolder, PluginCommands, LabelOverride,NSLOCTEXT("ContentBrowser", "ExploreTooltip", "Finds this folder on disk."),
      	                                    FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.BrowseContent"));
      }
      
    • 必须获取内容浏览器命令扩展委托添加命令 才会响应快捷键

      FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
      
      	TArray<FContentBrowserCommandExtender>& CBCommandExtenderDelegates = ContentBrowserModule.GetAllContentBrowserCommandExtenders();
      	CBCommandExtenderDelegates.Add(FContentBrowserCommandExtender::CreateRaw(this, &FMyAssetToolsModule::OnExtendContentBrowserCommands));
      	ContentBrowserCommandExtenderDelegateHandle = CBCommandExtenderDelegates.Last().GetHandle();
      
    • 可以只添加命令 不添加菜单 通过快捷键响应更符合需求

  • Window磁盘路径不对

    • 通过内容浏览器返回选中的数据结构 再去查找Window磁盘路径 有些文件夹没办法查找正确

      const FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
      
      TArray<FAssetData> SelectedAssets;
      ContentBrowserModule.Get().GetSelectedAssets(SelectedAssets);
      
      TArray<FString> SelectedFolders;
      ContentBrowserModule.Get().GetSelectedFolders(SelectedFolders);
      
      
      for (auto SelectedAsset : SelectedAssets)
      {
          // 获取资源后缀 .uasset .map
          const FString* PackageExtension = SelectedAsset.GetPackage()->ContainsMap() ? &FPackageName::GetMapPackageExtension() : &FPackageName::GetAssetPackageExtension();
      
          FString OutFilename;
      
          // 获取相对路径 ../../../../Content/xxx/xxx/aaa.uasset
          FPackageName::TryConvertLongPackageNameToFilename(SelectedAsset.PackageName.ToString(), OutFilename, *PackageExtension);
      
          // 获取绝对路径 D:/Project/Content/xxx/xxx/aaa.uasset
          const FString AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*OutFilename);
      
          FPlatformProcess::ExploreFolder(*AbsolutePath);
      }
      
      //挂载路径转换为虚拟路径 /All/Plugins -> /Plugins or /All/Game -> /Game */
      TArray<FString> InvariantPaths;
      for (const FString& VirtualPath : SelectedFolders)
      {
          FString InvariantPath;
          if (IContentBrowserDataModule::Get().GetSubsystem()->TryConvertVirtualPath(VirtualPath, InvariantPath) == EContentBrowserPathType::Internal)
          {
              InvariantPaths.Add(InvariantPath);
          }
      }
      
      //这种方式没办法处理Class文件和引擎目录文件
      for (const FString& InvariantPath : InvariantPaths)
      {
          FString OutFilename;
          FPackageName::TryConvertLongPackageNameToFilename(InvariantPath, OutFilename);
      
          const FString AbsolutePath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*OutFilename);
      
          FPlatformProcess::ExploreFolder(*AbsolutePath);
      }
      
    • 必须获取内容浏览器中的FContentBrowserItem对象查找Window磁盘路径

    • 源码没有直接返回选中FContentBrowserItem对象的接口 除非改源码

    • 但是阅读源码发现ContentBrowserUtils::TryGetItemFromUserProvidedPath可以通过路径名返回对应FContentBrowserItem对象

    • 这个是搜索框的功能接口 可以直接拿来用

    • 在拷贝源码中的ContentBrowserUtils::ExploreFolders来打开目录

      void FMyAssetToolsModule::OnExtendContentBrowserCommands(TSharedRef<FUICommandList> CommandList, FOnContentBrowserGetSelection GetSelectionDelegate) const
      {
      	CommandList->MapAction(FMyAssetToolsCommands::Get().ExploreFolder, 
      		FExecuteAction::CreateLambda([this, GetSelectionDelegate]
      		{
      			TArray<FAssetData> SelectedAssets;
      			TArray<FString> SelectedFolders;
      			GetSelectionDelegate.Execute(SelectedAssets, SelectedFolders);
      
      			TArray<FContentBrowserItem> InItems;
      
      			for (auto SelectedAsset : SelectedAssets)
      			{
      				FString ObjectPathString =  SelectedAsset.GetObjectPathString();
      				InItems.Add(ContentBrowserUtils_TryGetItemFromUserProvidedPath(ObjectPathString));
      			}
      
      			for (auto SelectedFolder : SelectedFolders)
      			{
      				InItems.Add(ContentBrowserUtils_TryGetItemFromUserProvidedPath(SelectedFolder));
      			}
      
      			ContentBrowserUtils_ExploreFolders(InItems);
      
      			// 需要修改源码的实现方式
      			// TArray<FContentBrowserItem> SelectedItems;
      			// ContentBrowserModule.Get().GetSelectedItems(SelectedItems);
      			//
      			// ContentBrowserUtils_ExploreFolders(SelectedItems);
      		})
      	);
      }
      

完整代码

#pragma once

#include "Framework/Commands/Commands.h"
#include "MyAssetToolsStyle.h"

class FMyAssetToolsCommands : public TCommands<FMyAssetToolsCommands>
{
public:

	FMyAssetToolsCommands()
		: TCommands<FMyAssetToolsCommands>(TEXT("MyAssetTools"), NSLOCTEXT("Contexts", "MyAssetTools", "MyAssetTools Plugin"), NAME_None, FMyAssetToolsStyle::GetStyleSetName())
	{
	}

	// TCommands<> interface
	virtual void RegisterCommands() override;

public:
	TSharedPtr< FUICommandInfo> ExploreFolder;
};

#include "MyAssetToolsCommands.h"

#define LOCTEXT_NAMESPACE "FMyAssetToolsModule"

void FMyAssetToolsCommands::RegisterCommands()
{
	UI_COMMAND(ExploreFolder, "在浏览器中显示", "在磁盘上查找此资产。", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control | EModifierKey::Shift, EKeys::B));
}

#undef LOCTEXT_NAMESPACE

#pragma once

#include "ContentBrowserDelegates.h"
#include "Modules/ModuleManager.h"

class FToolBarBuilder;
class FMenuBuilder;

class FMyAssetToolsModule : public IModuleInterface
{
public:

	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
	
private:
	void OnExtendContentBrowserCommands(TSharedRef<FUICommandList> CommandList, FOnContentBrowserGetSelection GetSelectionDelegate) const;

private:
	TSharedPtr<class FUICommandList> PluginCommands;
	FDelegateHandle ContentBrowserCommandExtenderDelegateHandle;
};

#include "MyAssetTools.h"
#include "ContentBrowserModule.h"
#include "IContentBrowserDataModule.h"
#include "IContentBrowserSingleton.h"
#include "MyAssetToolsCommands.h"
#include "Misc/MessageDialog.h"
#include "ToolMenus.h"

static const FName MyAssetToolsTabName("MyAssetTools");

#define LOCTEXT_NAMESPACE "FMyAssetToolsModule"

void FMyAssetToolsModule::StartupModule()
{
	FMyAssetToolsCommands::Register();

	FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));

	TArray<FContentBrowserCommandExtender>& CBCommandExtenderDelegates = ContentBrowserModule.GetAllContentBrowserCommandExtenders();
	CBCommandExtenderDelegates.Add(FContentBrowserCommandExtender::CreateRaw(this, &FMyAssetToolsModule::OnExtendContentBrowserCommands));
	ContentBrowserCommandExtenderDelegateHandle = CBCommandExtenderDelegates.Last().GetHandle();
}

void FMyAssetToolsModule::ShutdownModule()
{
	FMyAssetToolsCommands::Unregister();

	if ((GIsEditor && !IsRunningCommandlet()) && UObjectInitialized() && FSlateApplication::IsInitialized())
	{
		FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
		
		TArray<FContentBrowserCommandExtender>& CBCommandExtenderDelegates = ContentBrowserModule.GetAllContentBrowserCommandExtenders();
		CBCommandExtenderDelegates.RemoveAll([this](const FContentBrowserCommandExtender& Delegate) { return Delegate.GetHandle() == ContentBrowserCommandExtenderDelegateHandle; });
	}
}

//ContentBrowserUtils类中的TryGetItemFromUserProvidedPath方法
FContentBrowserItem ContentBrowserUtils_TryGetItemFromUserProvidedPath(FStringView RequestedPathView)
{
	// For all types of accepted input we can trim a trailing slash if it exists
	if (RequestedPathView.EndsWith('/'))
	{
		RequestedPathView.LeftChopInline(1);
	}

	UContentBrowserDataSubsystem* ContentBrowserData = IContentBrowserDataModule::Get().GetSubsystem();
	FName RequestedPath(RequestedPathView);

	// If the path is already a valid virtual path, go there
	FContentBrowserItem Item = ContentBrowserData->GetItemAtPath(RequestedPath, EContentBrowserItemTypeFilter::IncludeAll);
	if (Item.IsValid())
	{
		return Item;
	}

	// If the path is a non-virtual path like /Game/Maps transform it into a virtual path and try and find an item there
	FName VirtualPath = ContentBrowserData->ConvertInternalPathToVirtual(RequestedPath);
	if (!VirtualPath.IsNone())
	{
		Item = ContentBrowserData->GetItemAtPath(VirtualPath, EContentBrowserItemTypeFilter::IncludeAll);
		if (Item.IsValid())
		{
			return Item;
		}
	}

	// If the string is a complete object path (with or without class), sync to that asset
	FStringView ObjectPathView = RequestedPathView;
	if (FPackageName::IsValidObjectPath(ObjectPathView) || FPackageName::ParseExportTextPath(RequestedPathView, nullptr, &ObjectPathView))
	{
		VirtualPath = ContentBrowserData->ConvertInternalPathToVirtual(FName(ObjectPathView));
		Item = ContentBrowserData->GetItemAtPath(VirtualPath, EContentBrowserItemTypeFilter::IncludeFiles);
		if (Item.IsValid())
		{
			return Item;
		}
	}

	auto GetItemFromPackageName = [ContentBrowserData](const FString& PackageName) -> FContentBrowserItem {
		// Packages like /Game/Characters/Knight do not map to virtual paths in data source, assets like /Game/Characters/Knight.Knight do.
		// See if there's an item if we duplicate the last part of the path 
		FName InternalPath(TStringBuilder<320>(InPlace, PackageName, TEXTVIEW("."), FPackageName::GetShortName(PackageName)));
		FName VirtualPath = ContentBrowserData->ConvertInternalPathToVirtual(InternalPath);
		FContentBrowserItem Item = ContentBrowserData->GetItemAtPath(VirtualPath, EContentBrowserItemTypeFilter::IncludeFiles);
		if (Item.IsValid())
		{
			return Item;
		}

		// Otherwise go up to the package path and enumerate items to see if there's an asset with the desired package name 
		FString PackagePath = FPackageName::GetLongPackagePath(PackageName);
		VirtualPath = ContentBrowserData->ConvertInternalPathToVirtual(FName(PackagePath));
		FContentBrowserDataFilter Filter;
		Filter.bRecursivePaths = false;
		Filter.ItemTypeFilter = EContentBrowserItemTypeFilter::IncludeFiles;
		ContentBrowserData->EnumerateItemsUnderPath(VirtualPath, Filter, [&Item, &PackageName](FContentBrowserItem&& InItem) { 
			FName InternalPath = InItem.GetInternalPath();	
			if (WriteToString<256>(InternalPath).ToView().StartsWith(PackageName))
			{
				Item = MoveTemp(InItem);
				return false; 
			}
			return true;
		});
		if (Item.IsValid())
		{
			return Item;
		}
		return FContentBrowserItem();
	};

	// If the string is an incomplete virtual path that looks more like a package name 
	// e.g. /All/Game/Maps/Arena rather than /Game/Maps/Arena or /All/Game/Maps/Arena.Arena
	// try and convert it to an internal path, then try and use it as a package name 
	{
		FName ConvertedPath;
		FString PackageName;
		if (ContentBrowserData->TryConvertVirtualPath(RequestedPath, ConvertedPath) == EContentBrowserPathType::Internal)
		{
			if (FPackageName::IsValidLongPackageName(WriteToString<256>(ConvertedPath)))
			{
				PackageName = ConvertedPath.ToString();
				Item = GetItemFromPackageName(PackageName);
				if (Item.IsValid())
				{
					return Item;
				}
			}
		}
	}

	// If the string is a filesystem path to a package, sync to that asset
	FString PackageName;
	if (FPackageName::IsValidLongPackageName(PackageName) || FPackageName::TryConvertFilenameToLongPackageName(FString(RequestedPathView), PackageName))
	{
		Item = GetItemFromPackageName(PackageName);
		if (Item.IsValid())
		{
			return Item;
		}
	}

	// Try and remove elements from the end of the path until it's a valid virtual path 
	FPathViews::IterateAncestors(RequestedPathView, [RequestedPathView, ContentBrowserData, &Item](FStringView InAncestor){
		if (RequestedPathView == InAncestor)
		{
			return true;
		}
		FName AncestorName(InAncestor);
		Item = ContentBrowserData->GetItemAtPath(AncestorName, EContentBrowserItemTypeFilter::IncludeFolders);
		if (Item.IsValid())
		{
			return false;
		}
		return true;
	});
	if (Item.IsValid())
	{
		return Item;
	}

	return FContentBrowserItem();
}

//ContentBrowserUtils类中的ExploreFolders方法
void ContentBrowserUtils_ExploreFolders(const TArray<FContentBrowserItem>& InItems)
{
	TArray<FString> ExploreItems;

	for (const FContentBrowserItem& SelectedItem : InItems)
	{
		FString ItemFilename;
		if (SelectedItem.GetItemPhysicalPath(ItemFilename))
		{
			const bool bExists = SelectedItem.IsFile() ? FPaths::FileExists(ItemFilename) : FPaths::DirectoryExists(ItemFilename);
			if (bExists)
			{
				ExploreItems.Add(IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*ItemFilename));
			}
		}
	}

	const int32 BatchSize = 10;
	const FText FileManagerName = FPlatformMisc::GetFileManagerName();
	const bool bHasMultipleBatches = ExploreItems.Num() > BatchSize;
	for (int32 i = 0; i < ExploreItems.Num(); ++i)
	{
		bool bIsBatchBoundary = (i % BatchSize) == 0;
		if (bHasMultipleBatches && bIsBatchBoundary)
		{
			int32 RemainingCount = ExploreItems.Num() - i;
			int32 NextCount = FMath::Min(BatchSize, RemainingCount);
			FText Prompt = FText::Format(LOCTEXT("ExecuteExploreConfirm", "Show {0} {0}|plural(one=item,other=items) in {1}?\nThere {2}|plural(one=is,other=are) {2} remaining."), NextCount, FileManagerName, RemainingCount);
			if (FMessageDialog::Open(EAppMsgType::YesNo, Prompt) != EAppReturnType::Yes)
			{
				return;
			}
		}

		FPlatformProcess::ExploreFolder(*ExploreItems[i]);
	}
}

void FMyAssetToolsModule::OnExtendContentBrowserCommands(TSharedRef<FUICommandList> CommandList, FOnContentBrowserGetSelection GetSelectionDelegate) const
{
	CommandList->MapAction(FMyAssetToolsCommands::Get().ExploreFolder, 
		FExecuteAction::CreateLambda([this, GetSelectionDelegate]
		{
			TArray<FAssetData> SelectedAssets;
			TArray<FString> SelectedFolders;
			GetSelectionDelegate.Execute(SelectedAssets, SelectedFolders);

			TArray<FContentBrowserItem> InItems;

			for (auto SelectedAsset : SelectedAssets)
			{
				FString ObjectPathString =  SelectedAsset.GetObjectPathString();
				InItems.Add(ContentBrowserUtils_TryGetItemFromUserProvidedPath(ObjectPathString));
			}

			for (auto SelectedFolder : SelectedFolders)
			{
				InItems.Add(ContentBrowserUtils_TryGetItemFromUserProvidedPath(SelectedFolder));
			}

			ContentBrowserUtils_ExploreFolders(InItems);
		})
	);
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FMyAssetToolsModule, MyAssetTools)

posted @ 2025-04-30 16:00  邹强  阅读(122)  评论(0)    收藏  举报