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)

浙公網安備 33010602011771號