Files
UnrealEngine/Engine/Source/Developer/SettingsEditor/Private/Widgets/SSettingsEditor.cpp
2025-05-18 13:04:45 +08:00

585 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Widgets/SSettingsEditor.h"
#include "UObject/UnrealType.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "Widgets/SBoxPanel.h"
#include "Layout/WidgetPath.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/Layout/SSeparator.h"
#include "Widgets/Layout/SSpacer.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Styling/AppStyle.h"
#include "AnalyticsEventAttribute.h"
#include "EngineAnalytics.h"
#include "Interfaces/IAnalyticsProvider.h"
#include "Widgets/Text/STextBlock.h"
#include "PropertyEditorModule.h"
#include "IDetailsView.h"
#include "Widgets/Input/SHyperlink.h"
#include "Widgets/SSettingsSectionHeader.h"
#include "SSettingsEditorCheckoutNotice.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/PlatformFile.h"
#include "Containers/Ticker.h"
#define LOCTEXT_NAMESPACE "SSettingsEditor"
/* SSettingsEditor structors
*****************************************************************************/
SSettingsEditor::~SSettingsEditor()
{
Model->OnSelectionChanged().RemoveAll(this);
SettingsContainer->OnCategoryModified().RemoveAll(this);
FInternationalization::Get().OnCultureChanged().RemoveAll(this);
}
/* SSettingsEditor interface
*****************************************************************************/
void SSettingsEditor::Construct( const FArguments& InArgs, const ISettingsEditorModelRef& InModel )
{
bIsActiveTimerRegistered = false;
Model = InModel;
SettingsContainer = InModel->GetSettingsContainer();
OnApplicationRestartRequiredDelegate = InArgs._OnApplicationRestartRequired;
// initialize settings view
FDetailsViewArgs DetailsViewArgs;
{
DetailsViewArgs.bAllowSearch = true;
DetailsViewArgs.bHideSelectionTip = true;
DetailsViewArgs.bLockable = false;
DetailsViewArgs.bSearchInitialKeyFocus = true;
DetailsViewArgs.bUpdatesFromSelection = false;
DetailsViewArgs.NotifyHook = this;
DetailsViewArgs.bShowOptions = true;
DetailsViewArgs.bShowModifiedPropertiesOption = false;
DetailsViewArgs.bShowAnimatedPropertiesOption = false;
DetailsViewArgs.bShowDifferingPropertiesOption = false;
DetailsViewArgs.bShowKeyablePropertiesOption = false;
DetailsViewArgs.bShowHiddenPropertiesWhilePlayingOption = false;
DetailsViewArgs.bShowPropertyMatrixButton = false;
DetailsViewArgs.bAllowMultipleTopLevelObjects = true;
DetailsViewArgs.bCustomNameAreaLocation = true;
DetailsViewArgs.bCustomFilterAreaLocation = true;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
}
SettingsView = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor").CreateDetailView(DetailsViewArgs);
SettingsView->SetVisibility(TAttribute<EVisibility>::CreateSP(this, &SSettingsEditor::HandleSettingsViewVisibility));
SettingsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateSP(this, &SSettingsEditor::HandleSettingsViewEnabled));
TSharedPtr<FSettingsDetailRootObjectCustomization> RootObjectCustomization = MakeShareable(new FSettingsDetailRootObjectCustomization(Model, SettingsView.ToSharedRef()));
RootObjectCustomization->Initialize();
SettingsView->SetRootObjectCustomizationInstance(RootObjectCustomization);
TSharedPtr<SScrollBox> ScrollBox;
ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(16.0f, 0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 16.0f)
[
// categories menu
SAssignNew(ScrollBox, SScrollBox)
+ SScrollBox::Slot()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SAssignNew(CategoriesBox, SVerticalBox)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SSpacer)
.Size(FVector2D(24.0f, 0.0f))
]
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(24.0f, 0.0f, 24.0f, 0.0f))
[
SNew(SSeparator)
.Orientation(Orient_Vertical)
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f, 16.0f)
[
SNew(SVerticalBox)
.Visibility(this, &SSettingsEditor::HandleSettingsBoxVisibility)
+ SVerticalBox::Slot()
.AutoHeight()
[
SettingsView->GetFilterAreaWidget().ToSharedRef()
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
// settings area
SNew(SOverlay)
+ SOverlay::Slot()
[
SettingsView.ToSharedRef()
]
+ SOverlay::Slot()
.Expose(CustomWidgetSlot)
]
]
]
];
ScrollBox->SetScrollBarRightClickDragAllowed(true);
FInternationalization::Get().OnCultureChanged().AddSP(this, &SSettingsEditor::HandleCultureChanged);
Model->OnSelectionChanged().AddSP(this, &SSettingsEditor::HandleModelSelectionChanged);
SettingsContainer->OnCategoryModified().AddSP(this, &SSettingsEditor::HandleSettingsContainerCategoryModified);
ReloadCategories();
}
/* FNotifyHook interface
*****************************************************************************/
void SSettingsEditor::NotifyPostChange( const FPropertyChangedEvent& PropertyChangedEvent, class FEditPropertyChain* PropertyThatChanged )
{
if (PropertyChangedEvent.ChangeType != EPropertyChangeType::Interactive)
{
// Note while there could be multiple objects in the details panel, only one is ever edited at once.
// There could be zero objects being edited in the FStructOnScope case.
if (PropertyChangedEvent.GetNumObjectsBeingEdited() > 0)
{
UObject* ObjectBeingEdited = (UObject*)PropertyChangedEvent.GetObjectBeingEdited(0);
// Get the section from the edited object. We cannot use the selected section as multiple sections can be shown at once in the settings details panel.
ISettingsSectionPtr Section = Model->GetSectionFromSectionObject(ObjectBeingEdited);
{
FString RelativePath;
bool bIsSourceControlled = false;
bool bIsNewFile = false;
// Attempt to checkout the file automatically
if (ObjectBeingEdited->GetClass()->HasAnyClassFlags(CLASS_DefaultConfig))
{
RelativePath = ObjectBeingEdited->GetDefaultConfigFilename();
bIsSourceControlled = true;
}
else if (ObjectBeingEdited->GetClass()->HasAnyClassFlags(CLASS_Config))
{
RelativePath = ObjectBeingEdited->GetClass()->GetConfigName();
}
FString FullPath = FPaths::ConvertRelativePathToFull(RelativePath);
if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*FullPath))
{
bIsNewFile = true;
}
if (!bIsSourceControlled || !SettingsHelpers::CheckOutOrAddFile(FullPath))
{
SettingsHelpers::MakeWritable(FullPath);
}
if (Section.IsValid())
{
RecordPreferenceChangedAnalytics(Section, PropertyChangedEvent);
}
// Determine if the Property is an Array or Array Element
bool bIsArrayOrArrayElement = PropertyThatChanged->GetActiveMemberNode()->GetValue()->IsA(FArrayProperty::StaticClass())
|| PropertyThatChanged->GetActiveMemberNode()->GetValue()->ArrayDim > 1
|| PropertyChangedEvent.Property->GetOwner<FArrayProperty>();
bool bIsSetOrSetElement = PropertyThatChanged->GetActiveMemberNode()->GetValue()->IsA(FSetProperty::StaticClass())
|| PropertyChangedEvent.Property->GetOwner<FSetProperty>();
bool bIsMapOrMapElement = PropertyThatChanged->GetActiveMemberNode()->GetValue()->IsA(FMapProperty::StaticClass())
|| PropertyChangedEvent.Property->GetOwner<FMapProperty>();
if (ObjectBeingEdited->GetClass()->HasAnyClassFlags(CLASS_DefaultConfig) && !bIsArrayOrArrayElement && !bIsSetOrSetElement && !bIsMapOrMapElement)
{
if (!Section.IsValid() || Section->NotifySectionOnPropertyModified())
{
ObjectBeingEdited->UpdateSinglePropertyInConfigFile(PropertyThatChanged->GetActiveMemberNode()->GetValue(), ObjectBeingEdited->GetDefaultConfigFilename());
}
}
else if (Section.IsValid())
{
Section->Save();
}
// Some files being edited might have an array element, but they may also not have a corresponding section for inlined
// external objects, for them if they're DefaultConfig, we update them here.
else if (ObjectBeingEdited->GetClass()->HasAnyClassFlags(CLASS_DefaultConfig))
{
ObjectBeingEdited->TryUpdateDefaultConfigFile();
}
if (bIsNewFile && bIsSourceControlled)
{
SettingsHelpers::CheckOutOrAddFile(FullPath);
}
static const FName ConfigRestartRequiredKey = "ConfigRestartRequired";
if (PropertyChangedEvent.Property->GetBoolMetaData(ConfigRestartRequiredKey) || PropertyChangedEvent.MemberProperty->GetBoolMetaData(ConfigRestartRequiredKey))
{
OnApplicationRestartRequiredDelegate.ExecuteIfBound();
}
}
}
}
}
/* SSettingsEditor implementation
*****************************************************************************/
TWeakObjectPtr<UObject> SSettingsEditor::GetSelectedSettingsObject() const
{
ISettingsSectionPtr SelectedSection = Model->GetSelectedSection();
if (SelectedSection.IsValid())
{
return SelectedSection->GetSettingsObject();
}
return nullptr;
}
TSharedRef<SWidget> SSettingsEditor::MakeCategoryWidget( const ISettingsCategoryRef& Category, const TArray<ISettingsSectionPtr>& InSections )
{
// create section widgets
TSharedRef<SVerticalBox> SectionsBox = SNew(SVerticalBox);
if (InSections.Num() == 0)
{
return SNullWidget::NullWidget;
}
// list the sections
for (const ISettingsSectionPtr& Section : InSections)
{
SectionsBox->AddSlot()
.HAlign(HAlign_Left)
.Padding(0.0f, 10.0f, 0.0f, 0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("TreeArrow_Collapsed_Hovered"))
.Visibility(this, &SSettingsEditor::HandleSectionLinkImageVisibility, Section)
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.VAlign(VAlign_Center)
[
SNew(SHyperlink)
.OnNavigate(this, &SSettingsEditor::HandleSectionLinkNavigate, Section)
.Text(Section->GetDisplayName())
.ToolTipText(Section->GetDescription())
]
];
if (!Model->GetSelectedSection().IsValid())
{
Model->SelectSection(Section);
}
}
// create category widget
return SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
// category title
SNew(STextBlock)
.Font(FAppStyle::GetFontStyle("SettingsEditor.CatgoryAndSectionFont"))
.Text(Category->GetDisplayName())
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
// sections list
SectionsBox
];
}
void SSettingsEditor::RecordPreferenceChangedAnalytics( ISettingsSectionPtr SelectedSection, const FPropertyChangedEvent& PropertyChangedEvent ) const
{
FProperty* ChangedProperty = PropertyChangedEvent.MemberProperty;
// submit analytics data
if(FEngineAnalytics::IsAvailable() && ChangedProperty != nullptr && ChangedProperty->GetOwnerClass() != nullptr)
{
TArray<FAnalyticsEventAttribute> EventAttributes;
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("PropertySection"), SelectedSection->GetName().ToString()));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("PropertyClass"), ChangedProperty->GetOwnerClass()->GetName()));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("PropertyName"), ChangedProperty->GetName()));
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.PreferencesChanged"), EventAttributes);
}
}
void SSettingsEditor::ReloadCategories()
{
CategoriesBox->ClearChildren();
CategoriesBox->AddSlot()
.AutoHeight()
.HAlign(HAlign_Left)
.Padding(0.0f, 5.0f, 0.0f, 18.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("TreeArrow_Collapsed_Hovered"))
.Visibility(this, &SSettingsEditor::HandleAllSectionsLinkImageVisibility)
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.VAlign(VAlign_Center)
[
SNew(SHyperlink)
.OnNavigate(this, &SSettingsEditor::HandleAllSectionsLinkNavigate)
.Text(LOCTEXT("AllPropertiesLink", "All Settings"))
.ToolTipText(LOCTEXT("AllPropertiesLink_Tooltip", "Show all settings"))
]
];
TArray<ISettingsCategoryPtr> Categories;
SettingsContainer->GetCategories(Categories);
TArray<TSharedPtr<ISettingsSection>> CategorySettingsSections;
TArray<UObject*> SettingsObjects;
for (const ISettingsCategoryPtr& Category : Categories)
{
CategorySettingsSections.Reset();
Category->GetSections(CategorySettingsSections);
// sort the sections alphabetically
struct FSectionSortPredicate
{
FORCEINLINE bool operator()(ISettingsSectionPtr A, ISettingsSectionPtr B) const
{
if (!A.IsValid() && !B.IsValid())
{
return false;
}
if (A.IsValid() != B.IsValid())
{
return B.IsValid();
}
return (A->GetDisplayName().CompareTo(B->GetDisplayName()) < 0);
}
};
CategorySettingsSections.Sort(FSectionSortPredicate());
TSharedRef<SWidget> CategoryWidget = MakeCategoryWidget(Category.ToSharedRef(), CategorySettingsSections);
if (CategoryWidget != SNullWidget::NullWidget)
{
CategoriesBox->AddSlot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 16.0f)
[
MoveTemp(CategoryWidget)
];
}
for(TSharedPtr<ISettingsSection>& Section : CategorySettingsSections)
{
UObject* SettingsObject = Section->GetSettingsObject().Get();
if(SettingsObject)
{
SettingsObjects.Add(SettingsObject);
}
}
}
SettingsView->SetObjects(SettingsObjects);
}
/* SSettingsEditor callbacks
*****************************************************************************/
void SSettingsEditor::HandleCultureChanged()
{
ReloadCategories();
}
void SSettingsEditor::HandleModelSelectionChanged()
{
// This callback can trigger on unregister during shutdown, simply return in this case
if (!FSlateApplication::IsInitialized())
{
return;
}
ISettingsSectionPtr SelectedSection = Model->GetSelectedSection();
if (SelectedSection.IsValid())
{
TSharedPtr<SWidget> CustomWidget = SelectedSection->GetCustomWidget().Pin();
// show settings widget
if (CustomWidget.IsValid())
{
CustomWidgetSlot->AttachWidget( CustomWidget.ToSharedRef() );
}
else
{
CustomWidgetSlot->AttachWidget( SNullWidget::NullWidget );
}
// focus settings widget
TSharedPtr<SWidget> FocusWidget;
if (CustomWidget.IsValid())
{
FocusWidget = CustomWidget;
}
else
{
FocusWidget = SettingsView;
}
FWidgetPath FocusWidgetPath;
if (FSlateApplication::Get().GeneratePathToWidgetUnchecked(FocusWidget.ToSharedRef(), FocusWidgetPath))
{
FSlateApplication::Get().SetKeyboardFocus(FocusWidgetPath, EFocusCause::SetDirectly);
}
bShowingAllSettings = false;
}
else
{
bShowingAllSettings = true;
CustomWidgetSlot->AttachWidget( SNullWidget::NullWidget );
}
// clear the global search terms when selecting a specific category
SettingsView->ClearSearch();
}
void SSettingsEditor::HandleSectionLinkNavigate( ISettingsSectionPtr Section )
{
Model->SelectSection(Section);
}
void SSettingsEditor::HandleAllSectionsLinkNavigate()
{
Model->SelectSection(nullptr);
SettingsView->RefreshRootObjectVisibility();
}
EVisibility SSettingsEditor::HandleAllSectionsLinkImageVisibility() const
{
return bShowingAllSettings ? EVisibility::Visible : EVisibility::Hidden;
}
EVisibility SSettingsEditor::HandleSectionLinkImageVisibility( ISettingsSectionPtr Section ) const
{
if (Model->GetSelectedSection() == Section)
{
return EVisibility::Visible;
}
return EVisibility::Hidden;
}
EVisibility SSettingsEditor::HandleSettingsBoxVisibility() const
{
ISettingsSectionPtr SelectedSection = Model->GetSelectedSection();
if (SelectedSection.IsValid() || bShowingAllSettings)
{
return EVisibility::Visible;
}
return EVisibility::Hidden;
}
void SSettingsEditor::HandleSettingsContainerCategoryModified( const FName& CategoryName )
{
if ( !bIsActiveTimerRegistered )
{
bIsActiveTimerRegistered = true;
FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateSP(this, &SSettingsEditor::UpdateCategoriesCallback));
}
}
bool SSettingsEditor::UpdateCategoriesCallback(float InDeltaTime)
{
bIsActiveTimerRegistered = false;
ReloadCategories();
return false;
}
bool SSettingsEditor::HandleSettingsViewEnabled() const
{
ISettingsSectionPtr SelectedSection = Model->GetSelectedSection();
return (SelectedSection.IsValid() && SelectedSection->CanEdit()) || bShowingAllSettings;
}
EVisibility SSettingsEditor::HandleSettingsViewVisibility() const
{
ISettingsSectionPtr SelectedSection = Model->GetSelectedSection();
if (bShowingAllSettings || (SelectedSection.IsValid() && SelectedSection->GetSettingsObject().IsValid()))
{
return EVisibility::Visible;
}
return EVisibility::Hidden;
}
#undef LOCTEXT_NAMESPACE