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)