Files
UnrealEngine/Engine/Source/Editor/InternationalizationSettings/Private/SCulturePicker.cpp
2025-05-18 13:04:45 +08:00

426 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SCulturePicker.h"
#include "Containers/Map.h"
#include "Framework/Views/ITypedTableView.h"
#include "HAL/Platform.h"
#include "Internationalization/Culture.h"
#include "Internationalization/Internationalization.h"
#include "Internationalization/Text.h"
#include "Layout/Children.h"
#include "Misc/Attribute.h"
#include "Misc/CString.h"
#include "SlotBase.h"
#include "Styling/SlateColor.h"
#include "Templates/Function.h"
#include "Templates/Tuple.h"
#include "Widgets/Input/SComboButton.h"
#include "Widgets/Input/SSearchBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SToolTip.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Views/STableRow.h"
class ITableRow;
#define LOCTEXT_NAMESPACE "CulturePicker"
namespace UE::Internationalization::Private
{
FString GetCultureDisplayName(const FCultureRef& Culture, SCulturePicker::ECultureDisplayFormat DisplayNameFormat, const bool bIsRootItem)
{
const FString DisplayName = Culture->GetDisplayName();
if (DisplayNameFormat == SCulturePicker::ECultureDisplayFormat::ActiveCultureDisplayName)
{
return DisplayName;
}
const FString NativeName = Culture->GetNativeName();
if (DisplayNameFormat == SCulturePicker::ECultureDisplayFormat::NativeCultureDisplayName)
{
return NativeName;
}
if (DisplayNameFormat == SCulturePicker::ECultureDisplayFormat::ActiveAndNativeCultureDisplayName)
{
// Only show both names if they're different (to avoid repetition), and we're a root item (to avoid noise)
return (bIsRootItem && !NativeName.Equals(DisplayName, ESearchCase::CaseSensitive))
? FString::Printf(TEXT("%s [%s]"), *DisplayName, *NativeName)
: DisplayName;
}
if (DisplayNameFormat == SCulturePicker::ECultureDisplayFormat::NativeAndActiveCultureDisplayName)
{
// Only show both names if they're different (to avoid repetition), and we're a root item (to avoid noise)
return (bIsRootItem && !NativeName.Equals(DisplayName, ESearchCase::CaseSensitive))
? FString::Printf(TEXT("%s [%s]"), *NativeName, *DisplayName)
: NativeName;
}
return DisplayName;
}
}
void SCulturePicker::Construct( const FArguments& InArgs )
{
OnCultureSelectionChanged = InArgs._OnSelectionChanged;
IsCulturePickable = InArgs._IsCulturePickable;
DisplayNameFormat = InArgs._DisplayNameFormat;
ViewMode = InArgs._ViewMode;
CanSelectNone = InArgs._CanSelectNone;
BuildStockEntries();
RebuildEntries();
ChildSlot
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
[
SNew(SSearchBox)
.HintText( LOCTEXT("SearchHintText", "Name/Abbreviation") )
.OnTextChanged(this, &SCulturePicker::OnFilterStringChanged)
.DelayChangeNotificationsWhileTyping(true)
]
+SVerticalBox::Slot()
.FillHeight(1.0f)
[
SAssignNew(TreeView, STreeView<TSharedPtr<FCultureEntry>>)
.SelectionMode(ESelectionMode::Single)
.TreeItemsSource(&RootEntries)
.OnGenerateRow(this, &SCulturePicker::OnGenerateRow)
.OnGetChildren(this, &SCulturePicker::OnGetChildren)
.OnSelectionChanged(this, &SCulturePicker::OnSelectionChanged)
]
];
if (TSharedPtr<FCultureEntry> InitialSelection = FindEntryForCulture(InArgs._InitialSelection))
{
TGuardValue<bool> SuppressSelectionGuard(SuppressSelectionCallback, true);
TreeView->SetSelection(InitialSelection);
}
AutoExpandEntries();
}
void SCulturePicker::RequestTreeRefresh()
{
RebuildEntries();
if (TreeView.IsValid())
{
TreeView->RequestTreeRefresh();
}
AutoExpandEntries();
}
TSharedPtr<FCultureEntry> SCulturePicker::FindEntryForCulture(FCulturePtr Culture) const
{
return FindEntryForCultureImpl(Culture, RootEntries);
}
TSharedPtr<FCultureEntry> SCulturePicker::FindEntryForCultureImpl(FCulturePtr Culture, const TArray<TSharedPtr<FCultureEntry>>& Entries) const
{
for (const TSharedPtr<FCultureEntry>& Entry : Entries)
{
if (Entry->Culture == Culture)
{
return Entry;
}
if (TSharedPtr<FCultureEntry> ChildEntry = FindEntryForCultureImpl(Culture, Entry->Children))
{
return ChildEntry;
}
}
return nullptr;
}
void SCulturePicker::AutoExpandEntries()
{
return AutoExpandEntriesImpl(RootEntries);
}
void SCulturePicker::AutoExpandEntriesImpl(const TArray<TSharedPtr<FCultureEntry>>& Entries)
{
if (ViewMode == ECulturesViewMode::Hierarchical && TreeView)
{
for (const TSharedPtr<FCultureEntry>& Entry : Entries)
{
if (Entry->AutoExpand)
{
TreeView->SetItemExpansion(Entry, true);
// Only recurse when actually filtering
if (!FilterString.IsEmpty())
{
AutoExpandEntriesImpl(Entry->Children);
}
}
}
}
}
void SCulturePicker::BuildStockEntries()
{
StockEntries.Empty();
TArray<FString> StockCultureNames;
FInternationalization::Get().GetCultureNames(StockCultureNames);
TMap<FString, TSharedPtr<FCultureEntry>> TopLevelStockCultureEntries;
TMap<FString, TSharedPtr<FCultureEntry>> AllStockCultureEntries;
AllStockCultureEntries.Reserve(StockCultureNames.Num());
for (const FString& CultureName : StockCultureNames)
{
const FCulturePtr Culture = FInternationalization::Get().GetCulture(CultureName);
if (Culture.IsValid())
{
TArray<FString> HierarchicalCultureNames = Culture->GetPrioritizedParentCultureNames();
if (HierarchicalCultureNames.Num() == 0 || HierarchicalCultureNames[0] != CultureName)
{
HierarchicalCultureNames.Remove(CultureName);
HierarchicalCultureNames.Insert(CultureName, 0);
}
// Walk the array backwards to process the cultures in parent->child order
TSharedPtr<FCultureEntry> ParentCultureEntry;
const int32 TopLevelCultureIndex = HierarchicalCultureNames.Num() - 1;
for (int32 CultureIndex = TopLevelCultureIndex; CultureIndex >= 0; --CultureIndex)
{
// Find the culture data
const FString HierarchicalCultureName = HierarchicalCultureNames[CultureIndex];
const FCulturePtr HierarchicalCulture = FInternationalization::Get().GetCulture(HierarchicalCultureName);
if (!HierarchicalCulture.IsValid())
{
continue;
}
// Find or add a map entry for this culture
TSharedPtr<FCultureEntry>& StockCultureEntryRef = AllStockCultureEntries.FindOrAdd(HierarchicalCultureName);
if (!StockCultureEntryRef.IsValid())
{
StockCultureEntryRef = MakeShareable(new FCultureEntry(HierarchicalCulture));
// Link this entry as a child of its parent
if (ParentCultureEntry.IsValid())
{
ParentCultureEntry->Children.Add(StockCultureEntryRef);
}
}
// Is this culture a top-level entry?
if (CultureIndex == TopLevelCultureIndex)
{
TSharedPtr<FCultureEntry>& TopLevelStockCultureEntryRef = TopLevelStockCultureEntries.FindOrAdd(HierarchicalCultureName);
if (!TopLevelStockCultureEntryRef.IsValid())
{
TopLevelStockCultureEntryRef = StockCultureEntryRef;
}
}
ParentCultureEntry = StockCultureEntryRef;
}
}
}
// Populate the top-level array
StockEntries.Reserve(TopLevelStockCultureEntries.Num());
for (const auto& CultureNameDataPair : TopLevelStockCultureEntries)
{
StockEntries.Add(CultureNameDataPair.Value);
}
// Sort entries
StockEntries.Sort([this](const TSharedPtr<FCultureEntry>& LHS, const TSharedPtr<FCultureEntry>& RHS) -> bool
{
const FString LHSDisplayName = GetCultureDisplayName(LHS->Culture.ToSharedRef(), false);
const FString RHSDisplayName = GetCultureDisplayName(RHS->Culture.ToSharedRef(), false);
return FTextComparison::CompareTo(LHSDisplayName, RHSDisplayName) < 0;
});
}
void SCulturePicker::RebuildEntries()
{
RootEntries.Reset();
if (CanSelectNone)
{
RootEntries.Add(MakeShareable(new FCultureEntry(nullptr, true)));
}
TFunction<void (const TArray< TSharedPtr<FCultureEntry> >&, TArray< TSharedPtr<FCultureEntry> >&)> DeepCopyAndFilter
= [&](const TArray< TSharedPtr<FCultureEntry> >& InEntries, TArray< TSharedPtr<FCultureEntry> >& OutEntries)
{
for (const TSharedPtr<FCultureEntry>& InEntry : InEntries)
{
// Set pickable flag.
bool IsPickable = true;
if (IsCulturePickable.IsBound())
{
IsPickable = IsCulturePickable.Execute(InEntry->Culture);
}
TSharedRef<FCultureEntry> OutEntry = MakeShareable(new FCultureEntry(InEntry->Culture, IsPickable));
// Recurse to children.
DeepCopyAndFilter(InEntry->Children, OutEntry->Children);
bool IsFilteredOut = false;
if (!FilterString.IsEmpty())
{
const FString Name = OutEntry->Culture->GetName();
const FString DisplayName = OutEntry->Culture->GetDisplayName();
const FString NativeName = OutEntry->Culture->GetNativeName();
IsFilteredOut = !Name.Contains(FilterString) && !DisplayName.Contains(FilterString) && !NativeName.Contains(FilterString);
}
switch (ViewMode)
{
case ECulturesViewMode::Hierarchical:
// If has children, must be added. If it is not filtered and it is pickable, should be added.
if (OutEntry->Children.Num() != 0 || (!IsFilteredOut && IsPickable))
{
OutEntry->AutoExpand = (!IsPickable || IsFilteredOut) && OutEntry->Children.Num() > 0;
OutEntries.Add(OutEntry);
}
break;
case ECulturesViewMode::Flat:
// Add this entry only if it's valid, but always append its children (as they have already been validated)
if (IsPickable && !IsFilteredOut)
{
OutEntries.Add(OutEntry);
}
OutEntries.Append(OutEntry->Children);
OutEntry->Children.Reset();
break;
default:
checkf(false, TEXT("Unknown ECulturesViewMode!"));
break;
}
}
};
DeepCopyAndFilter(StockEntries, RootEntries);
}
void SCulturePicker::OnFilterStringChanged(const FText& InFilterString)
{
FilterString = InFilterString.ToString();
RequestTreeRefresh();
}
TSharedRef<ITableRow> SCulturePicker::OnGenerateRow(TSharedPtr<FCultureEntry> Entry, const TSharedRef<STableViewBase>& Table)
{
return SNew(STableRow< TSharedPtr<FCultureEntry> >, Table)
[
SNew(STextBlock)
.Text(Entry->Culture.IsValid() ? FText::FromString(GetCultureDisplayName(Entry->Culture.ToSharedRef(), RootEntries.Contains(Entry))) : LOCTEXT("None", "None"))
.ToolTip(
SNew(SToolTip)
.Content()
[
SNew(STextBlock)
.Text(Entry->Culture.IsValid() ? FText::FromString(Entry->Culture->GetName()) : LOCTEXT("None", "None"))
.HighlightText( FText::FromString(FilterString) )
]
)
.HighlightText( FText::FromString(FilterString) )
.ColorAndOpacity( Entry->IsSelectable ? FSlateColor::UseForeground() : FSlateColor::UseSubduedForeground() )
];
}
void SCulturePicker::OnGetChildren(TSharedPtr<FCultureEntry> Entry, TArray< TSharedPtr<FCultureEntry> >& Children)
{
// Add entries from children array.
Children.Append(Entry->Children);
// Sort entries.
Children.Sort([this](const TSharedPtr<FCultureEntry>& LHS, const TSharedPtr<FCultureEntry>& RHS) -> bool
{
const FString LHSDisplayName = GetCultureDisplayName(LHS->Culture.ToSharedRef(), false);
const FString RHSDisplayName = GetCultureDisplayName(RHS->Culture.ToSharedRef(), false);
return FTextComparison::CompareTo(LHSDisplayName, RHSDisplayName) < 0;
});
}
void SCulturePicker::OnSelectionChanged(TSharedPtr<FCultureEntry> Entry, ESelectInfo::Type SelectInfo)
{
if (SuppressSelectionCallback)
{
return;
}
// Don't count as selection if the entry isn't actually selectable but is part of the hierarchy.
if (Entry.IsValid() && Entry->IsSelectable)
{
OnCultureSelectionChanged.ExecuteIfBound( Entry->Culture, SelectInfo );
}
}
FString SCulturePicker::GetCultureDisplayName(const FCultureRef& Culture, const bool bIsRootItem) const
{
return UE::Internationalization::Private::GetCultureDisplayName(Culture, DisplayNameFormat, bIsRootItem);
}
void SCulturePickerCombo::Construct(const FArguments& InArgs)
{
SelectedCulture = InArgs._SelectedCulture;
OnSelectionChanged = InArgs._OnSelectionChanged;
DisplayNameFormat = InArgs._DisplayNameFormat;
ChildSlot
[
SAssignNew(ComboButton, SComboButton)
.ButtonContent()
[
SNew(STextBlock)
.Font(InArgs._Font)
.Text(this, &SCulturePickerCombo::GetSelectedCultureDisplayName)
]
.MenuContent()
[
SNew(SBox)
.MaxDesiredHeight(400.0f)
.MaxDesiredWidth(300.0f)
[
SNew(SCulturePicker)
.InitialSelection(SelectedCulture.Get(nullptr))
.OnSelectionChanged(this, &SCulturePickerCombo::OnSelectedCultureChanged)
.IsCulturePickable(InArgs._IsCulturePickable)
.DisplayNameFormat(DisplayNameFormat)
.ViewMode(InArgs._ViewMode)
]
]
];
}
FText SCulturePickerCombo::GetSelectedCultureDisplayName() const
{
FCulturePtr SelectedCulturePtr = SelectedCulture.Get(nullptr);
return SelectedCulturePtr
? FText::AsCultureInvariant(UE::Internationalization::Private::GetCultureDisplayName(SelectedCulturePtr.ToSharedRef(), DisplayNameFormat, false))
: LOCTEXT("None", "None");
}
void SCulturePickerCombo::OnSelectedCultureChanged(FCulturePtr NewSelectedCulture, ESelectInfo::Type SelectInfo)
{
if (!SelectedCulture.IsBound())
{
SelectedCulture = NewSelectedCulture;
}
OnSelectionChanged.ExecuteIfBound(NewSelectedCulture, SelectInfo);
if (ComboButton)
{
ComboButton->SetIsOpen(false);
}
}
#undef LOCTEXT_NAMESPACE