Asset Pinner Tool

Project Overview
Across several projects, I noticed a recurring problem: as projects grew, increasingly complex folder structures made frequently used assets difficult to find. This led to the idea of creating a tool where developers could pin commonly used assets for quick access. From that idea, the Asset Pinner was born, a dockable panel that keeps frequently accessed assets readily available without having to navigate through the project's folder structure.
My Contributions
  • Designed and developed the Asset Pinner as a solo project using Unreal Engine C++ and Slate
  • Implemented a custom dockable area for pinning assets and folders for quick access
  • Added double-click navigation to open assets directly in their corresponding editors
  • Created custom context-menu actions for managing and locating pinned assets
  • Implemented an asset history for quickly accessing recently opened assets
  • Added custom tabs for organizing and categorizing pinned assets
Role
System & Tools Programmer
Duration
Ongoing
Team Size
1
Genre
Unreal Engine Tool
The core feature of the tool is the ability to pin and unpin assets and folders within a custom editor panel, providing quick access to frequently used content. Double-clicking a pinned asset opens it directly in its corresponding editor, while double-clicking a pinned folder navigates the Content Drawer to that location.The tool also includes a custom context menu containing the core pinning actions, along with additional functionality such as locating a pinned asset or folder directly in the Content Drawer.
void UPinnedAssetSubsystem::AddAssetPath(FString Path, EPathType Type, bool IsPinned)
{
	bool SaveData = false;
	if (!ContainsPath(Path))
	{
		AssetDataList.Add(FPinnedAssetData(Path, !IsPinned, Type));

		SaveData = true;

		if (OnListChangedDelegate.IsBound())
			OnListChangedDelegate.Execute(AssetDataList);
	}
	else
	{
		int Index = -1;
		if (FindPath(Path, Index))
		{
			if (AssetDataList[Index].TabIndex == 0 || !IsPinned)
				return;

			AssetDataList[Index].TabIndex = 0;

			SaveData = true;

			if (OnListChangedDelegate.IsBound())
				OnListChangedDelegate.Execute(AssetDataList);
		}
	}

	if (SaveData)
	{
		TArray<FString> SaveList;
		for (const auto& Data : AssetDataList)
		{
			SaveList.Add(Data.GetSaveString());
		}
		FFileHelper::SaveStringArrayToFile(SaveList, *FilePath);
	}
}

void UPinnedAssetSubsystem::RemoveAssetPath(FString Path)
{
	int Index = -1;
	if (FindPath(Path, Index))
	{
		AssetDataList.RemoveAt(Index);

		TArray<FString> SaveList;
		for (const auto& Data : AssetDataList)
		{
			SaveList.Add(Data.GetSaveString());
		}
		FFileHelper::SaveStringArrayToFile(SaveList, *FilePath);

		if (OnListChangedDelegate.IsBound())
			OnListChangedDelegate.Execute(AssetDataList);
	}
}

// Returns true if asset/folder is pinned, and false if asset/folder is recently opened
bool UPinnedAssetSubsystem::GetStatus(FString Path)
{
	int Index = -1;
	if (FindPath(Path, Index))
		return AssetDataList[Index].TabIndex != 1;

	return false;
}

// Asset or Folder
EPathType UPinnedAssetSubsystem::GetPathType(FString Path)
{
	int Index = -1;
	if (FindPath(Path, Index))
		return AssetDataList[Index].PathType;

	return EPathType::None;
}

const TArray<FPinnedAssetData>& UPinnedAssetSubsystem::GetAssetDataList()
{
	return AssetDataList;
}

bool UPinnedAssetSubsystem::ContainsPath(FString Path)
{
	for (const FPinnedAssetData& Data : AssetDataList)
		if (Data.AssetPath.Equals(Path))
			return true;

	return false;
}

bool UPinnedAssetSubsystem::FindPath(FString Path, int& OutIndex)
{
	int Index = 0;
	for (const FPinnedAssetData& Data : AssetDataList)
	{
		if (Data.AssetPath.Equals(Path))
		{
			OutIndex = Index;
			return true;
		}

		Index++;
	}

	OutIndex = -1;
	return false;
}

void UPinnedAssetSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);

	FolderIcon = LoadObject<UTexture2D>(nullptr, TEXT("/Script/Engine.Texture2D'/AssetPinner/Icon/Folder_Base_256x.Folder_Base_256x'"));
	FilePath = FPaths::GameUserDeveloperDir() + "PinnedAssetData.txt";
	TArray<FString> LoadList;
	FFileHelper::LoadFileToStringArray(LoadList, *FilePath);
	bool IsPinned = false;

	for (auto& line : LoadList)
	{
		TArray<FString> SplitString;
		line.ParseIntoArray(SplitString, TEXT(" "));
		AssetDataList.Add(FPinnedAssetData(
			SplitString[0], 
			FCString::Atoi(*SplitString[1]), 
			SplitString.IsValidIndex(2) ? (EPathType)FCString::Atoi(*SplitString[2]) : EPathType::Asset
		));
	}

	for (int i = 0; i < AssetDataList.Num(); i++)
	{
		FPackagePath OutPath;
		FPackagePath PackagePath;
		if (FPackagePath::TryFromPackageName(AssetDataList[i].AssetPath, PackagePath) || AssetDataList[i].PathType != EPathType::Asset)
		{
			if (!FPackageName::DoesPackageExist(PackagePath, &OutPath) && AssetDataList[i].PathType == EPathType::Asset)
			{
				UE_LOG(LogTemp, Warning, TEXT("Cannot find file: %s"), *AssetDataList[i].AssetPath);
				AssetDataList.RemoveAt(i);
				i--;
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Cannot find file: %s"), *AssetDataList[i].AssetPath);
			AssetDataList.RemoveAt(i);
			i--;
		}
	}
}
void PinAssetAction::PinAssets(const TArray<FAssetData>& SelectedAssets)
{
	for (auto& AssetData : SelectedAssets)
	{
		FString AssetPath = AssetData.PackageName.ToString();

		UPinnedAssetSubsystem* Subsystem = nullptr;
		if (GEditor)
			Subsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();

		if (Subsystem)
			Subsystem->AddAssetPath(AssetPath);
	}
}
void FAssetPinnerModule::AddContentBrowserContextMenuExtender()
{
    FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
    TArray<FContentBrowserMenuExtender_SelectedAssets>& CBMenuAssetExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
    TArray<FContentBrowserMenuExtender_SelectedPaths>& CBMenuPathExtenderDelegates = ContentBrowserModule.GetAllPathViewContextMenuExtenders();

    CBMenuAssetExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedAssets::CreateStatic(&OnExtendContentBrowserAssetSelectionMenu));
    ContentBrowserAssetExtenderDelegateHandle = CBMenuAssetExtenderDelegates.Last().GetHandle();
    CBMenuPathExtenderDelegates.Add(FContentBrowserMenuExtender_SelectedPaths::CreateStatic(&OnExtendContentBrowserPathSelectionMenu));
    ContentBrowserPathExtenderDelegateHandle = CBMenuPathExtenderDelegates.Last().GetHandle();
}

void FAssetPinnerModule::RemoveContentBrowserContextMenuExtender()
{
    FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));

    TArray<FContentBrowserMenuExtender_SelectedAssets>& CBMenuExtenderDelegates = ContentBrowserModule.GetAllAssetViewContextMenuExtenders();
    CBMenuExtenderDelegates.RemoveAll([this](const FContentBrowserMenuExtender_SelectedAssets& Delegate) { return Delegate.GetHandle() == ContentBrowserAssetExtenderDelegateHandle; });

    TArray<FContentBrowserMenuExtender_SelectedPaths>& CBMenuPathExtenderDelegates = ContentBrowserModule.GetAllPathViewContextMenuExtenders();
    CBMenuPathExtenderDelegates.RemoveAll([this](const FContentBrowserMenuExtender_SelectedPaths& Delegate) { return Delegate.GetHandle() == ContentBrowserPathExtenderDelegateHandle; });
}

void FAssetPinnerModule::AddMenuExtention(FMenuBuilder& MenuBuilder)
{
    MenuBuilder.BeginSection("Test section", LOCTEXT("ASSET_CONTEXT", "Pin Asset"));
    {
        // Add Menu Entry Here
        MenuBuilder.AddMenuEntry(
            LOCTEXT("ButtonName", "Test"),
            LOCTEXT("Button ToolTip", "A test button"),
            FSlateIcon(FAppStyle::GetAppStyleSetName(), "ViewportActorPreview.Pinned"),
            FUIAction(FExecuteAction::CreateLambda([]()
                {
                    FAssetPinnerModule::PrintString();
                })),
            NAME_None,
            EUserInterfaceActionType::Button);
    }
    MenuBuilder.EndSection();
}

TSharedRef<FExtender> FAssetPinnerModule::OnExtendContentBrowserAssetSelectionMenu(const TArray<FAssetData>& SelectedAssets)
{
    TSharedRef<FExtender> Extender = MakeShared<FExtender>();
    Extender->AddMenuExtension(
        "AssetContextCollections",
        EExtensionHook::After,
        nullptr,
        FMenuExtensionDelegate::CreateStatic(&ExecutePinAsset, SelectedAssets)
    );
    return Extender;
}

TSharedRef<FExtender> FAssetPinnerModule::OnExtendContentBrowserPathSelectionMenu(const TArray<FString>& SelectedAssets)
{
    TSharedRef<FExtender> Extender = MakeShared<FExtender>();
    Extender->AddMenuExtension(
        "PathContextBulkOperations",
        EExtensionHook::After,
        nullptr,
        FMenuExtensionDelegate::CreateStatic(&ExecutePinPath, SelectedAssets)
    );
    return Extender;
}

void FAssetPinnerModule::ExecutePinAsset(FMenuBuilder& MenuBuilder, const TArray<FAssetData> SelectedAssets)
{
	MenuBuilder.BeginSection("Pin Asset", LOCTEXT("ASSET_CONTEXT", "Pin Asset"));
	{
		// Add Menu Entry Here
		MenuBuilder.AddMenuEntry(
			LOCTEXT("ButtonName", "Pin"),
			LOCTEXT("Button ToolTip", "Pin Asset"),
			FSlateIcon(FAppStyle::GetAppStyleSetName(), "ViewportActorPreview.Pinned"),
			FUIAction(FExecuteAction::CreateLambda([SelectedAssets]()
				{
                    for (auto& AssetData : SelectedAssets)
                    {
                        FString AssetPath = AssetData.PackageName.ToString();

                        UPinnedAssetSubsystem* Subsystem = nullptr;
                        if (GEditor)
                            Subsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();

                        if (Subsystem)
                            Subsystem->AddAssetPath(AssetPath);
                    }
				})),
			NAME_None,
			EUserInterfaceActionType::Button);
	}
	MenuBuilder.EndSection();
}

void FAssetPinnerModule::ExecutePinPath(FMenuBuilder& MenuBuilder, const TArray<FString> SelectedAssets)
{
    MenuBuilder.BeginSection("Pin Asset", LOCTEXT("ASSET_CONTEXT", "Pin Asset"));
    {
        // Add Menu Entry Here
        MenuBuilder.AddMenuEntry(
            LOCTEXT("ButtonName", "Pin Path"),
            LOCTEXT("Button ToolTip", "Pin Path"),
            FSlateIcon(FAppStyle::GetAppStyleSetName(), "ViewportActorPreview.Pinned"),
            FUIAction(FExecuteAction::CreateLambda([SelectedAssets]()
                {
                    UPinnedAssetSubsystem* Subsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
                    if (Subsystem)
                        Subsystem->AddAssetPath(SelectedAssets[0], EPathType::Folder);
                })),
            NAME_None,
            EUserInterfaceActionType::Button);
    }
    MenuBuilder.EndSection();
}
void UPinnedWindowBase::NativeConstruct()
{
	Super::NativeConstruct();
	
	if (!GEngine)
		return;

	PinnedAssetSubsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
	if (!PinnedAssetSubsystem)
		return;

	PinnedAssetSubsystem->OnListChangedDelegate.BindDynamic(this, &UPinnedWindowBase::OnListChangedCallback);

	TArray<FString> Data;
	ConfigPath = FPaths::GameUserDeveloperDir() + "PinnedAssetConfig.txt";
	if (FPaths::ValidatePath(ConfigPath))
	{
		FFileHelper::LoadFileToStringArray(Data, *ConfigPath);
		if (!Data.IsEmpty())
		{
			if (Data[0].IsNumeric())
			{
				Size = FCString::Atof(*Data[0]);
				Data.RemoveAt(0);
			}
		}
	}

	if (PinnedSection)
		SectionMap.Add(FSection("Pinned", PinnedSection, true));

	if (RecentSection)
		SectionMap.Add(FSection("Recent", RecentSection, true));

	if (TabList)
	{
		UTab* PinnedTab = CreateWidget<UTab>(this, TabWidget);
		PinnedTab->SetInfo(FText::FromString("Pinned"), this, PinnedSection, true);
		PinnedTab->OnTabClickedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabClicked);
		ActiveTab = PinnedTab;
		ActiveTab->SetSelected(true);
		UTab* HistoryTab = CreateWidget<UTab>(this, TabWidget);
		HistoryTab->OnTabClickedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabClicked);
		HistoryTab->SetInfo(FText::FromString("History"), this, RecentSection, true);

		TabList->AddChild(PinnedTab);
		TabList->AddChild(HistoryTab);
	}

	Refresh(PinnedAssetSubsystem->GetAssetDataList());

	if (NewTabButton)
		NewTabButton->OnClicked.AddDynamic(this, &UPinnedWindowBase::OnNewTabClicked);
}

void UPinnedWindowBase::NativeDestruct()
{
	FString SaveConfig;
	SaveConfig += FString::SanitizeFloat(Size) + '\n';

	FFileHelper::SaveStringToFile(SaveConfig, *ConfigPath);
}

EditState UPinnedWindowBase::CheckInEditMode()
{
	return EditMode;
}

void UPinnedWindowBase::OnListChangedCallback(const TArray<FPinnedAssetData>& List)
{
	Refresh(List);
}

void UPinnedWindowBase::Refresh(const TArray<FPinnedAssetData>& List)
{
	if (!AssetSlotWidget) return;

	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();

	Slots.Empty();
	for (auto& Section : SectionMap)
		Section.SectionWidget->ClearPinnedAsset();

	for (const auto& Data : List)
	{
		TArray<FAssetData> Assets;
		AssetRegistry.GetAssetsByPackageName(FName(Data.AssetPath), Assets);
		if (Assets.Num() <= 0 && Data.PathType == EPathType::Asset)
			continue;

		UPinnedAssetSlotBase* NewSlot = CreateWidget<UPinnedAssetSlotBase>(this, AssetSlotWidget);
		NewSlot->SetAssetData(Data);
		NewSlot->SetThumbnail(Assets.IsValidIndex(0) ? Assets[0] : nullptr, PinnedAssetSubsystem);
		NewSlot->SetSize(Size, Size * Ratio);
		Slots.Add(NewSlot);

		SectionMap[Data.TabIndex].SectionWidget->AddPinnedAsset(NewSlot);
	}

	return;
}

FReply UPinnedWindowBase::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
	if (InKeyEvent.GetKey() == EKeys::LeftControl)
	{
		EditMode = EditState::InEditMode;
		PinnedSection->SetEnableScrolling(false);
		RecentSection->SetEnableScrolling(false);
	}
	return FReply::Handled();
}

FReply UPinnedWindowBase::NativeOnKeyUp(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
	if (InKeyEvent.GetKey() == EKeys::LeftControl)
	{
		EditMode = EditState::NotInEditMode;
		PinnedSection->SetEnableScrolling(true);
		RecentSection->SetEnableScrolling(true);
	}
	return FReply::Handled();
}

FReply UPinnedWindowBase::NativeOnMouseWheel(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	if (EditMode == EditState::InEditMode)
	{
		Size += InMouseEvent.GetWheelDelta() * 10;
		Size = FMath::Max(Size, MinSize);

		for (auto PinSlot : Slots)
		{
			PinSlot->SetSize(Size, Size * Ratio);
		}
		return FReply::Handled();
	}

	return FReply::Handled();
}

FReply UPinnedWindowBase::NativeOnFocusReceived(const FGeometry& InGeometry, const FFocusEvent& InFocusEvent)
{
	EditMode = EditState::NotInEditMode;
	return FReply::Handled();
}

void UPinnedWindowBase::NativeOnFocusLost(const FFocusEvent& InFocusEvent)
{
	EditMode = EditState::Unfocused;
}
void UPinnedSectionBase::ClearPinnedAsset()
{
	WrapBox->ClearChildren();
}

void UPinnedSectionBase::AddPinnedAsset(UPinnedAssetSlotBase* NewPinnedSlot)
{
	WrapBox->AddChildToWrapBox(NewPinnedSlot);
}

void UPinnedSectionBase::SetEnableScrolling(bool IsEnabled)
{
	ScrollBox->SetIsEnabled(IsEnabled);
}
void UPinnedAssetSlotBase::SetAssetData(const FPinnedAssetData& Data)
{
	AssetPath = Data.AssetPath;

	if (Name)
		Name->SetText(FText::FromString(FPackageName::GetShortName(*Data.AssetPath)));

	Background->AssetPath = Data.AssetPath;

	PathType = Data.PathType;
}

void UPinnedAssetSlotBase::SetThumbnail(const FAssetData& AssetData, const UPinnedAssetSubsystem* PinnedAssetSubsystem)
{
	USizeBoxSlot* SlotPtr = nullptr;

	if (PathType == EPathType::Folder)
	{
		UImage* Image = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass(), TEXT("Thumbnail"));
		Image->SetBrushFromTexture(PinnedAssetSubsystem->FolderIcon);
		SlotPtr = Cast<USizeBoxSlot>(ThumbnailHolder->AddChild(Image));
	}
	else if (PathType == EPathType::Asset)
	{
		Thumbnail = WidgetTree->ConstructWidget<UAssetThumbnailWidget>(UAssetThumbnailWidget::StaticClass(), TEXT("Thumbnail"));
		Thumbnail->SetAsset(AssetData);
		SlotPtr = Cast<USizeBoxSlot>(ThumbnailHolder->AddChild(Thumbnail));
	}

	if (SlotPtr)
	{
		SlotPtr->SetHorizontalAlignment(EHorizontalAlignment::HAlign_Fill);
		SlotPtr->SetVerticalAlignment(EVerticalAlignment::VAlign_Fill);
	}
}


FString UPinnedAssetSlotBase::GetAssetPath()
{
	return AssetPath;
}

void UPinnedAssetSlotBase::SetSize(int Width, int Height)
{
	SizeBox->SetHeightOverride(Height);
	SizeBox->SetWidthOverride(Width);

	if (Thumbnail)
		Thumbnail->SetResolution(FIntPoint(Width));
}

FReply UPinnedAssetSlotBase::NativeOnMouseButtonDoubleClick(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	if (!InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton))
		return FReply::Unhandled();

	if (PathType == EPathType::Folder)
	{
		TArray<FString> Assets{ AssetPath };

		FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
		ContentBrowserModule.Get().SyncBrowserToFolders(Assets);

		return FReply::Handled();
	}

	TArray<FAssetData> Assets;
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
	AssetRegistry.GetAssetsByPackageName(FName(AssetPath), Assets);

	if (Assets.Num() <= 0)
		return FReply::Handled();

	UObject* Asset = Assets[0].GetAsset();

	if (!Asset)
	{
		UE_LOG(LogTemp, Error, TEXT("Open Asset Window Failed - Asset is not valid"));
		return FReply::Handled();
	}

	UAssetEditorSubsystem* Subsystem = GEditor ? GEditor->GetEditorSubsystem<UAssetEditorSubsystem>() : nullptr;
	if (!Subsystem)
	{
		UE_LOG(LogTemp, Error, TEXT("Open Asset Window Failed - Asset Editor Subsystem is not valid"));
		return FReply::Handled();
	}

	bool success = Subsystem->OpenEditorForAsset(Asset);
	if (success)
	{
		UE_LOG(LogTemp, Log, TEXT("Open Asset Window Succeeded"));
	}
	else
		UE_LOG(LogTemp, Error, TEXT("Open Asset Window Failed"));

	return FReply::Handled();
}

void UPinnedAssetSlotBase::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	Background->SetBrushColor(HoverColor);
}

void UPinnedAssetSlotBase::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
	Background->SetBrushColor(BaseColor);
}
USTRUCT()
struct ASSETPINNER_API FPinnedAssetData
{
	GENERATED_BODY()

	FPinnedAssetData() = default;

	FPinnedAssetData(FString InAssetPath, int InTabIndex, EPathType InPathType)
	{
		AssetPath = InAssetPath;
		TabIndex = InTabIndex;
		PathType = InPathType;
	}

	FString GetSaveString() const { return AssetPath + ' ' + FString::FromInt(TabIndex) + ' ' + FString::FromInt((int)PathType); };

	FString AssetPath;
	int TabIndex;
	EPathType PathType;
};

USTRUCT()
struct FSection
{
	GENERATED_BODY()

	FString Name;

	UPROPERTY()
	UPinnedSectionBase* SectionWidget;

	UPROPERTY()
	bool bIsPersistent;

	FSection() = default;

	FSection(FString InName, UPinnedSectionBase* InSectionWidget, bool InIsPersistent = false)
	{
		Name = InName;
		SectionWidget = InSectionWidget;
		bIsPersistent = InIsPersistent;
	}
};
FReply SExtendedSlateBorder::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
	FReply Reply = FReply::Unhandled();
	
	if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
	{
		if (MyGeometry.IsUnderLocation(MouseEvent.GetScreenSpacePosition()))
		{
			// Right clicked, so summon a context menu if the cursor is within the widget
			FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath();

			TSharedPtr<SWidget> MenuContentWidget = BuildContextMenuContent();
			if (MenuContentWidget.IsValid())
			{
				static const bool bFocusImmediately = true;

				TSharedPtr<IMenu> ContextMenu;

					ContextMenu = FSlateApplication::Get().PushMenu(
						MouseEvent.GetWindow(),
						WidgetPath,
						MenuContentWidget.ToSharedRef(),
						MouseEvent.GetScreenSpacePosition(),
						FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu),
						bFocusImmediately
					);
			}
		}

		// Release mouse capture
		Reply = FReply::Handled();
	}

	return Reply;
}

TSharedPtr<SWidget> SExtendedSlateBorder::BuildContextMenuContent()
{
#define LOCTEXT_NAMESPACE "AssetPinnerContextMenu"
	// Set the menu to automatically close when the user commits to a choice
	const bool bShouldCloseWindowAfterMenuSelection = true;

	// This is a context menu which could be summoned from within another menu if this text block is in a menu
	// it should not close the menu it is inside
	bool bCloseSelfOnly = true;
	FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, TSharedPtr< const FUICommandList >(), nullptr, bCloseSelfOnly, &FCoreStyle::Get());
	{
		ContextMenuExtender.ExecuteIfBound(MenuBuilder);
	}

	return MenuBuilder.MakeWidget();
#undef LOCTEXT_NAMESPACE
}
TSharedRef<SWidget> UExtendedBorder::RebuildWidget()
{
	MyBorder = SNew(SExtendedSlateBorder)
		.FlipForRightToLeftFlowDirection(bFlipForRightToLeftFlowDirection)
		.ContextMenuExtender(FMenuExtensionDelegate::CreateUObject(this, &UExtendedBorder::ExtendContextMenu));

	if (GetChildrenCount() > 0)
	{
		Cast<UBorderSlot>(GetContentSlot())->BuildSlot(MyBorder.ToSharedRef());
	}

	return MyBorder.ToSharedRef();
}

void UExtendedBorder::ExtendContextMenu(FMenuBuilder& Builder)
{
#define LOCTEXT_NAMESPACE "AssetPinnerContextMenu"
	Builder.BeginSection("EditSection", LOCTEXT("Heading", "Edit Action"));
	{
		FUIAction PinAssetAction(
			FExecuteAction::CreateUObject(this, &UExtendedBorder::Pin),
			FCanExecuteAction::CreateUObject(this, &UExtendedBorder::CanPin)
		);

		FUIAction UnpinAssetAction(
			FExecuteAction::CreateUObject(this, &UExtendedBorder::Unpin)
		);

		Builder.AddMenuEntry(
			NSLOCTEXT("AssetPinner", "PinAssetLabel", "Pin Asset"),
			NSLOCTEXT("AssetPinner", "PinAssetTooltip", "Move the asset to pinned section"),
			FSlateIcon(),
			PinAssetAction
		);

		Builder.AddMenuEntry(
			NSLOCTEXT("AssetPinner", "UnpinAssetLabel", "Unpin Asset"),
			NSLOCTEXT("AssetPinner", "UnpinAssetTooltip", "Remove the asset from pinned section"),
			FSlateIcon(),
			UnpinAssetAction
		);

		FMenuBuilder TabMenuBuilder(true, TSharedPtr< const FUICommandList >(), nullptr, false, &FCoreStyle::Get());
	}
	Builder.EndSection();

	Builder.BeginSection("SearchSection", LOCTEXT("Heading", "Search Action"));
	{
		FUIAction LocateAssetAction(
			FExecuteAction::CreateUObject(this, &UExtendedBorder::LocateInBrowser)
		);

		Builder.AddMenuEntry(
			NSLOCTEXT("AssetPinner", "LocateAssetLabel", "Locate Asset"),
			NSLOCTEXT("AssetPinner", "LocateAssetTooltip", "Locate the asset in the content browser"),
			FSlateIcon(),
			LocateAssetAction
		);
	}
	Builder.EndSection();
#undef LOCTEXT_NAMESPACE
}

void UExtendedBorder::Pin()
{
	UPinnedAssetSubsystem* Subsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
	if (!Subsystem)
		return;

	Subsystem->MoveAssetPath(AssetPath, 0);
}


bool UExtendedBorder::CanPin()
{
	UPinnedAssetSubsystem* Subsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
	if (!Subsystem)
		return false;

	// False mean unpinned, and we only want this option available then
	return !Subsystem->GetStatus(AssetPath);
}

void UExtendedBorder::Unpin()
{
	UPinnedAssetSubsystem* Subsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
	if (!Subsystem)
		return;

	Subsystem->RemoveAssetPath(AssetPath);
}

void UExtendedBorder::LocateInBrowser()
{
	UPinnedAssetSubsystem* Subsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
	if (!Subsystem)
		return;

	FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
	if (Subsystem->GetPathType(AssetPath) == EPathType::Folder)
	{
		TArray<FString> Path{ AssetPath };
		ContentBrowserModule.Get().SyncBrowserToFolders(Path);
	}
	else
	{
		FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
		IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
		TArray<FAssetData> Assets;
		AssetRegistry.GetAssetsByPackageName(FName(AssetPath), Assets);
		if (Assets.Num() <= 0)
			return;

		ContentBrowserModule.Get().SyncBrowserToAssets(Assets);
	}
}
void UPinnedWindowBase::OnClearButtonClicked()
{
	PinnedAssetSubsystem->ClearRecent();
}
void UPinnedAssetSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);

	UAssetEditorSubsystem* AssetEditorSubsystem = GEditor ? GEditor->GetEditorSubsystem<UAssetEditorSubsystem>() : nullptr;
	if (!AssetEditorSubsystem)
	{
		UE_LOG(LogTemp, Error, TEXT("Bind Event Failed - Asset Editor Subsystem is not valid"));
		return;
	}

	AssetEditorSubsystem->OnAssetEditorRequestedOpen().AddUObject(this, &UPinnedAssetSubsystem::OnAssetEditorOpen);
}

void UPinnedAssetSubsystem::OnAssetEditorOpen(UObject* Asset)
{
	FAssetData AssetData(Asset);

	AddAssetPath(AssetData.PackageName.ToString(), EPathType::Asset, false);
}

void UPinnedAssetSubsystem::ClearRecent()
{
	AssetDataList.RemoveAll(
		[](FPinnedAssetData Candidate)
		{
			return Candidate.TabIndex == 1;
		}
	);

	TArray<FString> SaveList;
	for (const auto& Data : AssetDataList)
	{
		SaveList.Add(Data.GetSaveString());
	}
	FFileHelper::SaveStringArrayToFile(SaveList, *FilePath);

	if (OnListChangedDelegate.IsBound())
		OnListChangedDelegate.Execute(AssetDataList);
}
The tool also includes a history section that keeps track of recently opened assets, allowing users to quickly access them or move frequently used assets into the pinned section. Assets can be opened directly from the history by clicking their icons, and the entire history can be cleared with a single button when it is no longer needed.
The latest addition to the tool is support for custom tabs, allowing users to better organize their pinned assets. New tabs can be created using the “+” button and given a custom name. Pinned assets can then be moved between tabs through the “Move To” option in their context menu.

Custom tabs can also be removed using the “X” button beside each tab. When a tab is removed, its pinned assets are automatically transferred back to the main pinned section to prevent them from being lost.

This feature is currently work in progress, with some UI formatting issues still being addressed.
void UPinnedAssetSubsystem::MoveAssetPath(FString Path, int TabIndex)
{
	int Index = -1;
	if (FindPath(Path, Index))
	{
		if (AssetDataList[Index].TabIndex == TabIndex)
			return;

		AssetDataList[Index].TabIndex = TabIndex;

		TArray<FString> SaveList;
		for (const auto& Data : AssetDataList)
		{
			SaveList.Add(Data.GetSaveString());
		}
		FFileHelper::SaveStringArrayToFile(SaveList, *FilePath);

		if (OnListChangedDelegate.IsBound())
			OnListChangedDelegate.Execute(AssetDataList);
	}
}

void UPinnedAssetSubsystem::SetTabs(const TArray<FSection>& InTabs)
{
	for (const auto& Tab : InTabs)
	{
		Tabs.AddUnique(Tab.Name);
	}
}

void UPinnedAssetSubsystem::AddTabNames(FString Name)
{
	Tabs.AddUnique(Name);
}

void UPinnedAssetSubsystem::RemoveTab(int Index)
{
	if (Tabs.IsValidIndex(Index))
		Tabs.RemoveAt(Index);

	bool Modified = false;
	for (auto& AssetData : AssetDataList)
	{
		if (AssetData.TabIndex == Index)
		{
			AssetData.TabIndex = 0;
			Modified = true;
		}
		else if (AssetData.TabIndex > Index)
		{
			AssetData.TabIndex--;
			Modified = true;
		}
	}

	if (Modified)
	{
		TArray<FString> SaveList;
		for (const auto& Data : AssetDataList)
		{
			SaveList.Add(Data.GetSaveString());
		}
		FFileHelper::SaveStringArrayToFile(SaveList, *FilePath);

		if (OnListChangedDelegate.IsBound())
			OnListChangedDelegate.Execute(AssetDataList);
	}
}

void UPinnedAssetSubsystem::RenameTab(FString Name, int Index)
{
	if (Tabs.IsValidIndex(Index))
		Tabs[Index] = Name;
}

void UPinnedAssetSubsystem::EmptyTabName()
{
	Tabs.Empty();
}

TArray<FString> UPinnedAssetSubsystem::GetTabNames()
{
	return Tabs;
}
void UPinnedWindowBase::NativeConstruct()
{
	Super::NativeConstruct();

	TArray<FString> Data;
	ConfigPath = FPaths::GameUserDeveloperDir() + "PinnedAssetConfig.txt";
	if (FPaths::ValidatePath(ConfigPath))
	{
		FFileHelper::LoadFileToStringArray(Data, *ConfigPath);
		if (!Data.IsEmpty())
		{
			if (Data[0].IsNumeric())
			{
				Size = FCString::Atof(*Data[0]);
				Data.RemoveAt(0);
			}
		}
	}

	if (PinnedSectionWidget)
	{
		for (auto& line : Data)
		{
			UPinnedSectionBase* NewSection = CreateWidget<UPinnedSectionBase>(this, PinnedSectionWidget);
			UWidgetSwitcherSlot* NewSlot = Cast<UWidgetSwitcherSlot>(TabController->AddChild(NewSection));
			NewSlot->SetHorizontalAlignment(EHorizontalAlignment::HAlign_Fill);
			NewSlot->SetVerticalAlignment(EVerticalAlignment::VAlign_Fill);

			SectionMap.Add(FSection(line, NewSection));

			UTab* NewTab = CreateWidget<UTab>(this, TabWidget);
			NewTab->OnTabClickedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabClicked);
			NewTab->OnNameChangedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabRenamed);
			NewTab->OnRemoveClickedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabRemoved);
			NewTab->SetInfo(FText::FromString(line), this, NewSection);

			TabList->AddChild(NewTab);
		}
	}
	PinnedAssetSubsystem->SetTabs(SectionMap);
}

void UPinnedWindowBase::NativeDestruct()
{
	FString SaveConfig;
	SaveConfig += FString::SanitizeFloat(Size) + '\n';

	for (auto& Section : SectionMap)
		if(!Section.bIsPersistent)
			SaveConfig += Section.Name + '\n';

	FFileHelper::SaveStringToFile(SaveConfig, *ConfigPath);

	PinnedAssetSubsystem->EmptyTabName();
}

void UPinnedWindowBase::SwitchTab(UWidget* Widget)
{
	TabController->SetActiveWidget(Widget);
}

void UPinnedWindowBase::Refresh(const TArray<FPinnedAssetData>& List)
{
	if (!AssetSlotWidget) return;

	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
	IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();

	Slots.Empty();
	for (auto& Section : SectionMap)
		Section.SectionWidget->ClearPinnedAsset();

	for (const auto& Data : List)
	{
		TArray<FAssetData> Assets;
		AssetRegistry.GetAssetsByPackageName(FName(Data.AssetPath), Assets);
		if (Assets.Num() <= 0 && Data.PathType == EPathType::Asset)
			continue;

		UPinnedAssetSlotBase* NewSlot = CreateWidget<UPinnedAssetSlotBase>(this, AssetSlotWidget);
		NewSlot->SetAssetData(Data);
		NewSlot->SetThumbnail(Assets.IsValidIndex(0) ? Assets[0] : nullptr, PinnedAssetSubsystem);
		NewSlot->SetSize(Size, Size * Ratio);
		Slots.Add(NewSlot);

		SectionMap[Data.TabIndex].SectionWidget->AddPinnedAsset(NewSlot);
	}

	return;
}

void UPinnedWindowBase::OnTabClicked(UTab* Initiator)
{
	ActiveTab->SetSelected();
	ActiveTab = Initiator;
	SwitchTab(ActiveTab->SetSelected(true));
}

void UPinnedWindowBase::OnTabRenamed(UTab* Initiator, FText OldName, FText NewName)
{
	Initiator->SetInfo(NewName);

	int Index;
	if (FindSection(OldName.ToString(), Index))
	{
		SectionMap[Index].Name = NewName.ToString();
		PinnedAssetSubsystem->RenameTab(NewName.ToString(), Index);
	}
}

void UPinnedWindowBase::OnTabRemoved(UTab* Initiator)
{
	UWidget* Section = Initiator->GetSection();

	int Index = -1;
	if (FindSection(Initiator->GetName(), Index))
	{
		SectionMap.RemoveAt(Index);
		PinnedAssetSubsystem->RemoveTab(Index);
	}

	TabController->RemoveChild(Section);
	TabList->RemoveChild(Initiator);
}

void UPinnedWindowBase::OnNewTabClicked()
{
	FString NewName = "NewTab_" + FString::FromInt(DefaultNameIndex);
	while (ContainsSection(NewName))
	{
		DefaultNameIndex++;
		NewName = "NewTab_" + FString::FromInt(DefaultNameIndex);
	}

	UPinnedSectionBase* NewSection = CreateWidget<UPinnedSectionBase>(this, PinnedSectionWidget);
	UWidgetSwitcherSlot* NewSlot = Cast<UWidgetSwitcherSlot>(TabController->AddChild(NewSection));
	NewSlot->SetHorizontalAlignment(EHorizontalAlignment::HAlign_Fill);
	NewSlot->SetVerticalAlignment(EVerticalAlignment::VAlign_Fill);

	SectionMap.Add(FSection(NewName, NewSection));

	UTab* NewTab = CreateWidget<UTab>(this, TabWidget);
	NewTab->OnTabClickedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabClicked);
	NewTab->OnNameChangedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabRenamed);
	NewTab->OnRemoveClickedDelegate.BindDynamic(this, &UPinnedWindowBase::OnTabRemoved);
	NewTab->SetInfo(FText::FromString(NewName), this, NewSection);
	NewTab->EditName(true);

	TabList->AddChild(NewTab);
	PinnedAssetSubsystem->AddTabNames(NewName);

	DefaultNameIndex++;
}

bool UPinnedWindowBase::FindSection(FString Name, int& OutIndex)
{
	int Index = 0;
	for (auto& Section : SectionMap)
	{
		if (Section.Name == Name)
		{
			OutIndex = Index;
			return true;
		}

		Index++;
	}

	OutIndex = -1;
	return false;
}

bool UPinnedWindowBase::ContainsSection(FString Name)
{
	for (auto& Section : SectionMap)
	{
		if (Section.Name == Name)
		{
			return true;
		}
	}

	return false;
}
void UExtendedBorder::ExtendContextMenu(FMenuBuilder& Builder)
{
#define LOCTEXT_NAMESPACE "AssetPinnerContextMenu"
	Builder.BeginSection("EditSection", LOCTEXT("Heading", "Edit Action"));
	{
		Builder.AddSubMenu(
			NSLOCTEXT("AssetPinner", "MoveAssetLabel", "Move Asset"),
			NSLOCTEXT("AssetPinner", "MoveAssetTooltip", "Move the asset to another tab"),
			FNewMenuDelegate::CreateUObject(this, &UExtendedBorder::GenerateTabSubMenu)
		);
	}
	Builder.EndSection();
#undef LOCTEXT_NAMESPACE
}

void UExtendedBorder::MoveToTab(int Index)
{
	UPinnedAssetSubsystem* PinnedAssetSubsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
	if (!PinnedAssetSubsystem)
		return;

	PinnedAssetSubsystem->MoveAssetPath(AssetPath, Index);
}

void UExtendedBorder::GenerateTabSubMenu(FMenuBuilder& Builder)
{
	UPinnedAssetSubsystem* PinnedAssetSubsystem = GEditor->GetEditorSubsystem<UPinnedAssetSubsystem>();
	if (!PinnedAssetSubsystem)
		return;

	TArray<FString> Names = PinnedAssetSubsystem->GetTabNames();

	for (int i = 0; i < Names.Num(); i++)
	{
		Builder.AddMenuEntry(
			NSLOCTEXT("AssetPinner", "SwitchTabLabel", "Move To " + Names[i]),
			NSLOCTEXT("AssetPinner", "SwitchTabTooltip", "Move Asset To " + Names[i] + " Tab"),
			FSlateIcon(),
			FExecuteAction::CreateUObject(this, &UExtendedBorder::MoveToTab, i)
		);
	}
}