Asset Pinner Tool

Project Overview
While participating in various projects, one recurring issue I've seen is messy folders, and highly nested directory. A lot of times, I have trouble finding assets in such projects, and this is when the idea came to my mind: What if I have a tool where I can dock the assets I use or modify frequently. And from that thought, this tool was born, a side area where developer can pin their frequently used assets.
My Contributions
This is a solo project, most of the things in this project I've created myself, with some code taken from Unreal Engine Source Code, such as how they setup an Slate Widget. As of right now, I've implemented features such as docking an asset in an custom area, double click to open corresponding asset editor, a custom context menu with action such as unpin and locate asset, ability to pin folder, a history section for recently opened assets, custom tabs to sort pinned assets.
Role
System & Tools Programmer
Duration
Ongoing
Team Size
1
Genre
Unreal Engine Tool
The tool comes with a few different features, the core feature is the ability to pin/unpin asset and folder to a custom editor utility widget. User also have the ability to open corresponding editors by double clicking on the pinned asset, or redirect the content drawer to specific folder when double clicking a pinned folder. The tool also contains a custom context menu with core feature as options, as well as an extra feature to locate the pinned asset or folder 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 have a section which stores all recently opened asset, with the ability to move them into the pinned section. Users also have the ability to open the corresponding editor, by clicking on the icons in the history section. If the history section ever became too cluttered, users can simply remove everything by clicking on the clear button
The latest feature of the tool, the ability for users to add custom tabs to better sort the pinned assets. Users can add new tabs by clicking on the "plus" icon at the end of the tabs slider, and name the tabs based on their liking. Users can transfer pinned asset by right clicking on an asset, and select the "move to" option. The ability to remove tabs is also included, next to each custom tabs is a "x" mark which will remove the tabs, and transfer all pinned assets in it to the main pinned section.

This feature is still work-in-progress, there is a bug where the editable text box for the tab is not formatted corrected as seen in the video.
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)
		);
	}
}