// Copyright Epic Games, Inc. All Rights Reserved. #include "Widgets/SSettingsSectionHeader.h" #include "ISettingsCategory.h" #include "Widgets/SBoxPanel.h" #include "Framework/Notifications/NotificationManager.h" #include "HAL/PlatformFileManager.h" #include "Misc/MessageDialog.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Input/SButton.h" #include "Widgets/Text/STextBlock.h" #include "Styling/AppStyle.h" #include "ISourceControlModule.h" #include "SSettingsEditorCheckoutNotice.h" #include "DesktopPlatformModule.h" #include "IDetailsView.h" #include "Widgets/Views/SExpanderArrow.h" #include "Widgets/Views/STableRow.h" #include "HAL/IConsoleManager.h" #define LOCTEXT_NAMESPACE "SSettingsEditor" // Workaround to hide Set As Default button until there's a better way to determine if config file is cooked // Currently the only way to know is to check if MakeDefaultConfigFileWritable returns false, but that can't happen before the button is clicked bool bHideSetAsDefaultButton = false; FAutoConsoleVariableRef CVarHideSetAsDefaultButton(TEXT("SettingsEditor.HideSetAsDefaultButton"), bHideSetAsDefaultButton, TEXT("Hide the Settings Editor button to save to default config.")); void SSettingsSectionHeader::Construct(const FArguments& InArgs, const UObject* InSettingsObject, ISettingsEditorModelPtr InModel, TSharedPtr InDetailsView, const TSharedPtr& InTableRow) { Model = InModel; SettingsObject = MakeWeakObjectPtr(const_cast(InSettingsObject)); SettingsSection = Model->GetSectionFromSectionObject(InSettingsObject); DetailsView = InDetailsView; TableRow = InTableRow; Model->OnSelectionChanged().AddSP(this, &SSettingsSectionHeader::OnSettingsSelectionChanged); // Create the watcher widget for the default config file (checks file status / SCC state) FileWatcherWidget = SNew(SSettingsEditorCheckoutNotice) .Visibility(this, &SSettingsSectionHeader::GetCheckoutNoticeVisibility) .OnFileProbablyModifiedExternally(this, &SSettingsSectionHeader::HandleCheckoutNoticeFileProbablyModifiedExternally) .ConfigFilePath(this, &SSettingsSectionHeader::HandleCheckoutNoticeConfigFilePath); ChildSlot .Padding(FMargin(0.0f, 8.0f, 0.0f, 5.0f)) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f, 2.0f, 2.0f, 2.0f) .AutoWidth() [ SNew(SExpanderArrow, InTableRow) ] + SHorizontalBox::Slot()// category title [ SNew(STextBlock) .Font(FAppStyle::GetFontStyle("SettingsEditor.CatgoryAndSectionFont")) .Text(GetSettingsBoxTitleText()) .OverflowPolicy(ETextOverflowPolicy::MiddleEllipsis) ] ] +SVerticalBox::Slot() .Padding(FMargin(0.f, 8.f, 0.f, 0.f)) .AutoHeight() .MinHeight(TAttribute(this, &SSettingsSectionHeader::GetDescriptionRowMinHeight)) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Fill) .Padding(20.0f, 0.0f, 0.0f, 0.0f) [ SNew(SBorder) .Padding(2.f, 0.f, 0.f, 0.f) .VAlign(VAlign_Center) .BorderImage(FAppStyle::GetBrush("NoBorder")) .Visibility(this, &SSettingsSectionHeader::GetContentVisibility) [ // category description SNew(STextBlock) .ColorAndOpacity(FSlateColor::UseSubduedForeground()) .Text(GetSettingsBoxDescriptionText()) .OverflowPolicy(ETextOverflowPolicy::Ellipsis) ] ] + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Right) .VAlign(VAlign_Center) .Padding(16.0f, 0.0f, 0.0f, 0.0f) [ SNew(SHorizontalBox) .Visibility(this, &SSettingsSectionHeader::GetButtonRowVisibility) +SHorizontalBox::Slot() [ // set as default button SNew(SButton) .Visibility(this, &SSettingsSectionHeader::HandleSetAsDefaultButtonVisibility) .IsEnabled(this, &SSettingsSectionHeader::HandleSetAsDefaultButtonEnabled) .OnClicked(this, &SSettingsSectionHeader::HandleSetAsDefaultButtonClicked) .Text(LOCTEXT("SaveDefaultsButtonText", "Set as Default")) .ToolTipText(LOCTEXT("SaveDefaultsButtonTooltip", "Save the values below as the new default settings")) ] +SHorizontalBox::Slot() .AutoWidth() .Padding(8.0f, 0.0f, 0.0f, 0.0f) [ // export button SNew(SButton) .IsEnabled(this, &SSettingsSectionHeader::HandleExportButtonEnabled) .OnClicked(this, &SSettingsSectionHeader::HandleExportButtonClicked) .Text(LOCTEXT("ExportButtonText", "Export...")) .ToolTipText(LOCTEXT("ExportButtonTooltip", "Export these settings to a file on your computer")) ] +SHorizontalBox::Slot() .AutoWidth() .Padding(8.0f, 0.0f, 0.0f, 0.0f) [ // import button SNew(SButton) .IsEnabled(this, &SSettingsSectionHeader::HandleImportButtonEnabled) .OnClicked(this, &SSettingsSectionHeader::HandleImportButtonClicked) .Text(LOCTEXT("ImportButtonText", "Import...")) .ToolTipText(LOCTEXT("ImportButtonTooltip", "Import these settings from a file on your computer")) ] +SHorizontalBox::Slot() .AutoWidth() .Padding(8.0f, 0.0f, 0.0f, 0.0f) [ // reset defaults button SNew(SButton) .Visibility(this, &SSettingsSectionHeader::HandleSetAsDefaultButtonVisibility) .IsEnabled(this, &SSettingsSectionHeader::HandleResetToDefaultsButtonEnabled) .OnClicked(this, &SSettingsSectionHeader::HandleResetDefaultsButtonClicked) .Text(LOCTEXT("ResetDefaultsButtonText", "Reset to Defaults")) .ToolTipText(LOCTEXT("ResetDefaultsButtonTooltip", "Reset the settings below to their default values")) ] ] ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 8.0f, 0.0f, 0.0f) [ FileWatcherWidget.ToSharedRef() ] ]; } FText SSettingsSectionHeader::GetSettingsBoxTitleText() const { if(SettingsSection.IsValid()) { static const FText TitleFmt = FText::FromString(TEXT("{0} - {1}")); return FText::Format(TitleFmt, SettingsSection->GetCategory().Pin()->GetDisplayName(), SettingsSection->GetDisplayName()); } return FText::GetEmpty(); } FText SSettingsSectionHeader::GetSettingsBoxDescriptionText() const { if(SettingsSection.IsValid()) { return SettingsSection->GetDescription(); } return FText::GetEmpty(); } EVisibility SSettingsSectionHeader::GetButtonRowVisibility() const { const EVisibility DefaultVisibility = GetContentVisibility(); if (DefaultVisibility != EVisibility::Visible) { return DefaultVisibility; } else { return DetailsView.Pin()->HasActiveSearch() ? EVisibility::Collapsed : EVisibility::Visible; } } EVisibility SSettingsSectionHeader::GetContentVisibility() const { return TableRow.IsValid() && TableRow.Pin()->IsItemExpanded() ? EVisibility::Visible : EVisibility::Collapsed; } FReply SSettingsSectionHeader::HandleExportButtonClicked() { if(SettingsSection.IsValid()) { if(LastExportDir.IsEmpty()) { LastExportDir = FPaths::GetPath(GEditorPerProjectIni); } FString DefaultFileName = FString::Printf(TEXT("%s Backup %s.ini"), *SettingsSection->GetDisplayName().ToString(), *FDateTime::Now().ToString(TEXT("%Y-%m-%d %H%M%S"))); TArray OutFiles; TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); void* ParentWindowHandle = (ParentWindow.IsValid() && ParentWindow->GetNativeWindow().IsValid()) ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() : nullptr; if(FDesktopPlatformModule::Get()->SaveFileDialog(ParentWindowHandle, LOCTEXT("ExportSettingsDialogTitle", "Export settings...").ToString(), LastExportDir, DefaultFileName, TEXT("Config files (*.ini)|*.ini"), EFileDialogFlags::None, OutFiles)) { if(SettingsSection->Export(OutFiles[0])) { ShowNotification(LOCTEXT("ExportSettingsSuccess", "Export settings succeeded"), SNotificationItem::CS_Success); } else { ShowNotification(LOCTEXT("ExportSettingsFailure", "Export settings failed"), SNotificationItem::CS_Fail); } } } return FReply::Handled(); } float SSettingsSectionHeader::GetDescriptionRowMinHeight() const { if (GetContentVisibility() == EVisibility::Visible) { return 22.f; // so that we keep a constant height even if the buttons are collapsed } else { return 0.f; } } bool SSettingsSectionHeader::HandleExportButtonEnabled() const { if(SettingsSection.IsValid()) { return SettingsSection->CanExport(); } return false; } FReply SSettingsSectionHeader::HandleImportButtonClicked() { if(SettingsSection.IsValid()) { TArray OutFiles; TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); void* ParentWindowHandle = (ParentWindow.IsValid() && ParentWindow->GetNativeWindow().IsValid()) ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() : nullptr; if(FDesktopPlatformModule::Get()->OpenFileDialog(ParentWindowHandle, LOCTEXT("ImportSettingsDialogTitle", "Import settings...").ToString(), FPaths::GetPath(GEditorPerProjectIni), TEXT(""), TEXT("Config files (*.ini)|*.ini"), EFileDialogFlags::None, OutFiles)) { if(SettingsSection->Import(OutFiles[0]) && SettingsSection->Save()) { ShowNotification(LOCTEXT("ImportSettingsSuccess", "Import settings succeeded"), SNotificationItem::CS_Success); } else { ShowNotification(LOCTEXT("ImportSettingsFailure", "Import settings failed"), SNotificationItem::CS_Fail); } } } return FReply::Handled(); } bool SSettingsSectionHeader::HandleImportButtonEnabled() const { if(SettingsSection.IsValid()) { bool CanImport = SettingsSection->CanEdit() && SettingsSection->CanImport(); if (SettingsObject->GetClass()->HasAnyClassFlags(CLASS_DefaultConfig)) { CanImport &= !IsDefaultConfigCheckOutNeeded(); } return CanImport; } return false; } /** * Gets the absolute path to the Default.ini for the specified object. * * @return The path to the file. */ FString SSettingsSectionHeader::GetDefaultConfigFilePath() const { FString RelativeConfigFilePath = SettingsObject->GetDefaultConfigFilename(); return FPaths::ConvertRelativePathToFull(RelativeConfigFilePath); } /** * Checks whether the default config file needs to be checked out for editing. * * @return true if the file needs to be checked out, false otherwise. */ bool SSettingsSectionHeader::IsDefaultConfigCheckOutNeeded(bool bForceSourceControlUpdate) const { if(SettingsObject.IsValid() && SettingsObject->GetClass()->HasAnyClassFlags(CLASS_Config | CLASS_DefaultConfig)) { // We can only fetch the file watcher if it's visible otherwise fallback to source control if (FileWatcherWidget->GetVisibility().IsVisible()) { return !FileWatcherWidget->IsUnlocked(); } else { return !SettingsHelpers::IsCheckedOut(GetDefaultConfigFilePath(), bForceSourceControlUpdate); } } else { return false; } } FReply SSettingsSectionHeader::HandleResetDefaultsButtonClicked() { if(SettingsSection.IsValid()) { SettingsSection->ResetDefaults(); } return FReply::Handled(); } bool SSettingsSectionHeader::HandleResetToDefaultsButtonEnabled() const { if(SettingsSection.IsValid()) { return (SettingsSection->CanEdit() && SettingsSection->CanResetDefaults()); } return false; } EVisibility SSettingsSectionHeader::HandleSetAsDefaultButtonVisibility() const { return (!bHideSetAsDefaultButton && (SettingsSection.IsValid() && SettingsSection->CanSaveDefaults())) ? EVisibility::Visible : EVisibility::Collapsed; } FReply SSettingsSectionHeader::HandleSetAsDefaultButtonClicked() { if(SettingsSection.IsValid()) { if(FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("SaveAsDefaultUserConfirm", "Are you sure you want to update the default settings?")) != EAppReturnType::Yes) { return FReply::Handled(); } bool FileNeedToBeAddedToSourceControl = false; FText SaveAsDefaultNeedsAddMessage = LOCTEXT("SaveAsDefaultNeedsAddMessage", "The default configuration file for these settings is currently not under revision control. Would you like to add it to revision control?"); FString DefaultConfigFilePath = GetDefaultConfigFilePath(); if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*DefaultConfigFilePath)) { if (IsDefaultConfigCheckOutNeeded(true)) { if (ISourceControlModule::Get().IsEnabled()) { FText DisplayMessage; if (SettingsHelpers::IsSourceControlled(DefaultConfigFilePath)) { DisplayMessage = LOCTEXT("SaveAsDefaultNeedsCheckoutMessage", "The default configuration file for these settings is currently not checked out. Would you like to check it out from revision control?"); } else { DisplayMessage = SaveAsDefaultNeedsAddMessage; } if (FMessageDialog::Open(EAppMsgType::YesNo, DisplayMessage) == EAppReturnType::Yes) { if (!CheckOutOrAddDefaultConfigFile()) { if (FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("SaveAsDefaultsSourceControlOperationFailed", "The revision control operation failed. Would you like to make it writable?")) == EAppReturnType::Yes) { MakeDefaultConfigFileWritable(); } else { return FReply::Handled(); } } } } else { if (FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("SaveAsDefaultsIsReadOnlyMessage", "The default configuration file for these settings is not currently writable. Would you like to make it writable?")) == EAppReturnType::Yes) { MakeDefaultConfigFileWritable(); } else { return FReply::Handled(); } } } } else { if (ISourceControlModule::Get().IsEnabled()) { FileNeedToBeAddedToSourceControl = true; } } SettingsSection->SaveDefaults(); if (FileNeedToBeAddedToSourceControl) { if (FMessageDialog::Open(EAppMsgType::YesNo, SaveAsDefaultNeedsAddMessage) == EAppReturnType::Yes) { if (!CheckOutOrAddDefaultConfigFile(true)) { FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("SaveAsDefaultsSourceControlFailedAddManually", "The revision control operation failed. You will need to add it manually")); return FReply::Handled(); } } } FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("SaveAsDefaultsSucceededMessage", "The default configuration file for these settings was updated successfully. \n\nIf checked into revision control this would affect other developers.")); } return FReply::Handled(); } bool SSettingsSectionHeader::HandleSetAsDefaultButtonEnabled() const { if(SettingsSection.IsValid()) { return SettingsSection->CanSaveDefaults(); } return false; } /** * Checks out the default configuration file for the currently selected settings object. * * @return true if the check-out succeeded, false otherwise. */ bool SSettingsSectionHeader::CheckOutOrAddDefaultConfigFile(bool bForceSourceControlUpdate) { if(!SettingsObject.IsValid()) { return false; } // check out configuration file FText ErrorMessage; FString AbsoluteConfigFilePath = GetDefaultConfigFilePath(); if (!SettingsHelpers::CheckOutOrAddFile(AbsoluteConfigFilePath, bForceSourceControlUpdate, false, &ErrorMessage)) { // show errors, if any if (!ErrorMessage.IsEmpty()) { FMessageDialog::Open(EAppMsgType::Ok, ErrorMessage); } return false; } return true; } /** * Makes the default configuration file for the currently selected settings object writable. * * @return true if it was made writable, false otherwise. */ bool SSettingsSectionHeader::MakeDefaultConfigFileWritable() { if(!SettingsObject.IsValid()) { return false; } FString AbsoluteConfigFilePath = GetDefaultConfigFilePath(); return SettingsHelpers::MakeWritable(AbsoluteConfigFilePath, true); } void SSettingsSectionHeader::ShowNotification(const FText& Text, SNotificationItem::ECompletionState CompletionState) const { FNotificationInfo Notification(Text); Notification.ExpireDuration = 3.f; Notification.bUseSuccessFailIcons = false; FSlateNotificationManager::Get().AddNotification(Notification)->SetCompletionState(CompletionState); } /** Returns the config file name currently being edited. */ FString SSettingsSectionHeader::HandleCheckoutNoticeConfigFilePath() const { if(SettingsObject.IsValid()) { if (SettingsObject->GetClass()->HasAnyClassFlags(CLASS_DefaultConfig)) { return GetDefaultConfigFilePath(); } else if (SettingsObject->GetClass()->HasAnyClassFlags(CLASS_Config)) { return SettingsObject->GetClass()->GetConfigName(); } } return FString(); } /** Reloads the configuration object. */ void SSettingsSectionHeader::HandleCheckoutNoticeFileProbablyModifiedExternally() { if(SettingsObject.IsValid() && SettingsObject->GetClass()->HasAnyClassFlags(CLASS_Config | CLASS_DefaultConfig)) { SettingsObject->ReloadConfig(); } } /** Callback for determining the visibility of the 'Locked' notice. */ EVisibility SSettingsSectionHeader::GetCheckoutNoticeVisibility() const { const EVisibility DefaultVisibility = GetContentVisibility(); if (DefaultVisibility != EVisibility::Visible) { return DefaultVisibility; } // Only DefaultConfig are under source control, so the checkout notice should not be visible for the other cases if(SettingsObject.IsValid() && SettingsObject->GetClass()->HasAnyClassFlags(CLASS_DefaultConfig) && !DetailsView.Pin()->HasActiveSearch()) { return EVisibility::Visible; } return EVisibility::Collapsed; } void SSettingsSectionHeader::OnSettingsSelectionChanged() { FileWatcherWidget->Invalidate(); } FSettingsDetailRootObjectCustomization::FSettingsDetailRootObjectCustomization(ISettingsEditorModelPtr InModel, const TSharedRef& InDetailsView) : Model(InModel) , DetailsView(InDetailsView) { } void FSettingsDetailRootObjectCustomization::Initialize() { Model->OnSelectionChanged().AddSP(this, &FSettingsDetailRootObjectCustomization::OnSelectedSectionChanged); // Call once to ensure we have a valid section object OnSelectedSectionChanged(); } TSharedPtr FSettingsDetailRootObjectCustomization::CustomizeObjectHeader(const FDetailsObjectSet& InRootObjectSet, const TSharedPtr& InTableRow) { return SNew(SSettingsSectionHeader, InRootObjectSet.RootObjects[0], Model, DetailsView.Pin(), InTableRow); } bool FSettingsDetailRootObjectCustomization::AreObjectsVisible(const FDetailsObjectSet& InRootObjectSet) const { return SelectedSectionObject == nullptr || SelectedSectionObject == InRootObjectSet.RootObjects[0] || DetailsView.Pin()->HasActiveSearch(); } bool FSettingsDetailRootObjectCustomization::ShouldDisplayHeader(const FDetailsObjectSet& InRootObjects) const { return true; } void FSettingsDetailRootObjectCustomization::OnSelectedSectionChanged() { ISettingsSectionPtr SelectedSection = Model->GetSelectedSection(); if(SelectedSection.IsValid()) { SelectedSectionObject = SelectedSection->GetSettingsObject(); SelectedSection->Select(); } else { SelectedSectionObject = nullptr; } DetailsView.Pin()->RefreshRootObjectVisibility(); } #undef LOCTEXT_NAMESPACE