// 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>) .SelectionMode(ESelectionMode::Single) .TreeItemsSource(&RootEntries) .OnGenerateRow(this, &SCulturePicker::OnGenerateRow) .OnGetChildren(this, &SCulturePicker::OnGetChildren) .OnSelectionChanged(this, &SCulturePicker::OnSelectionChanged) ] ]; if (TSharedPtr InitialSelection = FindEntryForCulture(InArgs._InitialSelection)) { TGuardValue SuppressSelectionGuard(SuppressSelectionCallback, true); TreeView->SetSelection(InitialSelection); } AutoExpandEntries(); } void SCulturePicker::RequestTreeRefresh() { RebuildEntries(); if (TreeView.IsValid()) { TreeView->RequestTreeRefresh(); } AutoExpandEntries(); } TSharedPtr SCulturePicker::FindEntryForCulture(FCulturePtr Culture) const { return FindEntryForCultureImpl(Culture, RootEntries); } TSharedPtr SCulturePicker::FindEntryForCultureImpl(FCulturePtr Culture, const TArray>& Entries) const { for (const TSharedPtr& Entry : Entries) { if (Entry->Culture == Culture) { return Entry; } if (TSharedPtr ChildEntry = FindEntryForCultureImpl(Culture, Entry->Children)) { return ChildEntry; } } return nullptr; } void SCulturePicker::AutoExpandEntries() { return AutoExpandEntriesImpl(RootEntries); } void SCulturePicker::AutoExpandEntriesImpl(const TArray>& Entries) { if (ViewMode == ECulturesViewMode::Hierarchical && TreeView) { for (const TSharedPtr& 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 StockCultureNames; FInternationalization::Get().GetCultureNames(StockCultureNames); TMap> TopLevelStockCultureEntries; TMap> AllStockCultureEntries; AllStockCultureEntries.Reserve(StockCultureNames.Num()); for (const FString& CultureName : StockCultureNames) { const FCulturePtr Culture = FInternationalization::Get().GetCulture(CultureName); if (Culture.IsValid()) { TArray 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 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& 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& 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& LHS, const TSharedPtr& 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 >&, TArray< TSharedPtr >&)> DeepCopyAndFilter = [&](const TArray< TSharedPtr >& InEntries, TArray< TSharedPtr >& OutEntries) { for (const TSharedPtr& InEntry : InEntries) { // Set pickable flag. bool IsPickable = true; if (IsCulturePickable.IsBound()) { IsPickable = IsCulturePickable.Execute(InEntry->Culture); } TSharedRef 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 SCulturePicker::OnGenerateRow(TSharedPtr Entry, const TSharedRef& Table) { return SNew(STableRow< TSharedPtr >, 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 Entry, TArray< TSharedPtr >& Children) { // Add entries from children array. Children.Append(Entry->Children); // Sort entries. Children.Sort([this](const TSharedPtr& LHS, const TSharedPtr& 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 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