// 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("PropertyEditor").CreateDetailView(DetailsViewArgs); SettingsView->SetVisibility(TAttribute::CreateSP(this, &SSettingsEditor::HandleSettingsViewVisibility)); SettingsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateSP(this, &SSettingsEditor::HandleSettingsViewEnabled)); TSharedPtr RootObjectCustomization = MakeShareable(new FSettingsDetailRootObjectCustomization(Model, SettingsView.ToSharedRef())); RootObjectCustomization->Initialize(); SettingsView->SetRootObjectCustomizationInstance(RootObjectCustomization); TSharedPtr 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(); bool bIsSetOrSetElement = PropertyThatChanged->GetActiveMemberNode()->GetValue()->IsA(FSetProperty::StaticClass()) || PropertyChangedEvent.Property->GetOwner(); bool bIsMapOrMapElement = PropertyThatChanged->GetActiveMemberNode()->GetValue()->IsA(FMapProperty::StaticClass()) || PropertyChangedEvent.Property->GetOwner(); 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 SSettingsEditor::GetSelectedSettingsObject() const { ISettingsSectionPtr SelectedSection = Model->GetSelectedSection(); if (SelectedSection.IsValid()) { return SelectedSection->GetSettingsObject(); } return nullptr; } TSharedRef SSettingsEditor::MakeCategoryWidget( const ISettingsCategoryRef& Category, const TArray& InSections ) { // create section widgets TSharedRef 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 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 Categories; SettingsContainer->GetCategories(Categories); TArray> CategorySettingsSections; TArray 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 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& 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 CustomWidget = SelectedSection->GetCustomWidget().Pin(); // show settings widget if (CustomWidget.IsValid()) { CustomWidgetSlot->AttachWidget( CustomWidget.ToSharedRef() ); } else { CustomWidgetSlot->AttachWidget( SNullWidget::NullWidget ); } // focus settings widget TSharedPtr 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