// Copyright (c) Meta Platforms, Inc. and affiliates. #pragma once #include "OculusXRProjectSetupToolWidget.h" #include "DetailColumnSizeData.h" #include "OculusXRProjectSetupToolModule.h" #include "OculusXRPSTEvents.h" #include "OculusXRPSTSettings.h" #include "OculusXRPSTUtils.h" #include "OculusXRTelemetry.h" #include "SWarningOrErrorBox.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Input/SButton.h" #include "Styling/SlateStyle.h" #define LOCTEXT_NAMESPACE "OculusXRProjectSetupToolWidget" /** * Construct the layout for the project setup tool tab * * @param InArgs [in] the arguments associated with this tool */ void SOculusXRProjectSetupToolWidget::Construct(const FArguments& InArgs) { Refresh(); // Populate the platform filter PlatformFilters.Add(MetaQuest_All); PlatformFilters.Add(ESetupRulePlatform::MetaLink); PlatformFilters.Add(ESetupRulePlatform::MetaQuest_3); PlatformFilters.Add(ESetupRulePlatform::MetaQuest_Pro); PlatformFilters.Add(ESetupRulePlatform::MetaQuest_2); const auto Settings = GetMutableDefault(); UpdateActiveTimer(Settings->bBackGroundChecks); // Apply the starting platform filter. const auto& Platform = static_cast(Settings->CurrentPlatform); CurrentPlatformFilterIndex = PlatformFilters.Find(Platform); if (CurrentPlatformFilterIndex == INDEX_NONE) { CurrentPlatformFilterIndex = 0; } // Setup the column information for the layout ColumnSizeData = MakeShared(); ColumnSizeData->SetValueColumnWidth(0.75f); ColumnSizeData->SetRightColumnMinWidth(150); // Construct the layout RootContainerWidget = SNew(SVerticalBox); BuildLayout(RootContainerWidget); ChildSlot [RootContainerWidget.ToSharedRef()]; } /** * Build the layout for the main window */ void SOculusXRProjectSetupToolWidget::BuildLayout(const TSharedPtr& RootContainer) { // Clear all existing contents RootContainer->ClearChildren(); const TSharedPtr HeaderContainer = SNew(SVerticalBox); // Section to contain the title bar BuildTitleSectionLayout(HeaderContainer); // Section to contain the filters BuildFilterSectionLayout(HeaderContainer, CurrentPlatformFilterIndex); const TSharedPtr RulesContainer = SNew(SScrollBox); // Section to contain the required rules BuildRequiredRulesSectionLayout(RulesContainer); // Section to contain the recommended rules BuildRecommendedRulesSectionLayout(RulesContainer); // Section to contain the applied rules BuildAppliedRulesSectionLayout(RulesContainer); // Section to contain the ignored rules BuildIgnoredRulesSectionLayout(RulesContainer); RootContainer->AddSlot() .AutoHeight() .Padding(0) [HeaderContainer.ToSharedRef()]; RootContainer->AddSlot() .Padding(0) [RulesContainer.ToSharedRef()]; // Section to show warning about restarting editor RootContainer->AddSlot() .AutoHeight() [SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(18.0f, 20.0f, 18.0f, 16.0f)) [SNew(SWarningOrErrorBox) .Visibility(this, &SOculusXRProjectSetupToolWidget::OnRestartEditorNoticeVisibility) .MessageStyle(EMessageStyle::Warning) .Message(LOCTEXT("PluginSettingsRestartNotice", "You must restart Unreal Editor for your changes to take effect.")) [SNew(SButton) .OnClicked(this, &SOculusXRProjectSetupToolWidget::OnRestartEditorButtonClicked) .TextStyle(FAppStyle::Get(), "NormalText") .Text(LOCTEXT("PluginSettingsRestartEditor", "Restart Now"))]]]; } /** * Build the title section layout */ void SOculusXRProjectSetupToolWidget::BuildTitleSectionLayout(const TSharedPtr& RootContainer) { const TSharedPtr TitleWidget = SNew(SHorizontalBox) .Clipping(EWidgetClipping::OnDemand); ProjectStatusWidget = SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle(TEXT("DetailsView.CategoryFontStyle"))) .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") .Text(FText()); UpdateProjectStatus(); TitleWidget->AddSlot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(8, 0, 0, 0) [ProjectStatusWidget.ToSharedRef()]; const TSharedPtr ButtonWidget = SNew(SHorizontalBox); auto Settings = GetMutableDefault(); FMenuBuilder DetailViewOptions(true, nullptr); DetailViewOptions.AddMenuEntry( LOCTEXT("BackGroundChecks", "Enable background rule checks"), LOCTEXT("BackGroundChecks_ToolTip", "If enabled background rule validity will be performed every 30 seconds."), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([Settings] { Settings->bBackGroundChecks = !Settings->bBackGroundChecks; Settings->SaveConfig(); const OculusXRTelemetry::TScopedMarker OptionEvent; const auto& Annotated = OptionEvent .AddAnnotation(OculusXRTelemetry::Annotations::Uid, "bBackGroundChecks") .AddAnnotation(OculusXRTelemetry::Annotations::Value, Settings->bBackGroundChecks ? "true" : "false"); }), FCanExecuteAction(), FIsActionChecked::CreateLambda([Settings]() -> bool { return Settings->bBackGroundChecks; })), NAME_None, EUserInterfaceActionType::ToggleButton); DetailViewOptions.AddMenuEntry( LOCTEXT("StopBuildOnUnappliedRequredItem", "Stop build on unapplied items"), LOCTEXT("StopBuildOnUnappliedRequredItem_ToolTip", "Stop build if required items are not applied or ignored."), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([Settings] { Settings->bStopBuildOnUnAppliedCriticalItems = !Settings->bStopBuildOnUnAppliedCriticalItems; Settings->SaveConfig(); const OculusXRTelemetry::TScopedMarker OptionEvent; const auto& Annotated = OptionEvent .AddAnnotation(OculusXRTelemetry::Annotations::Uid, "bStopBuildOnUnAppliedCriticalItems") .AddAnnotation(OculusXRTelemetry::Annotations::Value, Settings->bStopBuildOnUnAppliedCriticalItems ? "true" : "false"); }), FCanExecuteAction(), FIsActionChecked::CreateLambda([Settings]() -> bool { return Settings->bStopBuildOnUnAppliedCriticalItems; })), NAME_None, EUserInterfaceActionType::ToggleButton); ButtonWidget->AddSlot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Padding(FMargin(8.f, 0.f, 8.f, 0.f)) [SNew(SComboButton) .ContentPadding(0) .HasDownArrow(false) .ForegroundColor(FSlateColor::UseForeground()) .ComboButtonStyle(FAppStyle::Get(), "SimpleComboButton") .AddMetaData(FTagMetaData(TEXT("ViewOptions"))) .MenuContent() [DetailViewOptions.MakeWidget()] .ButtonContent() [SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Center) [SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.Toolbar.Settings")) .ColorAndOpacity(FSlateColor::UseForeground())]]]; // Top level container for the whole row const TSharedPtr RowWidget = SNew(SHorizontalBox); RowWidget->AddSlot() .HAlign(HAlign_Left) [TitleWidget.ToSharedRef()]; RowWidget->AddSlot() .HAlign(HAlign_Right) [ButtonWidget.ToSharedRef()]; // Add into the root container RootContainer->AddSlot() .AutoHeight() .Padding(0) [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.GridLine")) .Padding(FMargin(0, 0, 0, 1)) [SNew(SHorizontalBox) + SHorizontalBox::Slot() [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop")) .BorderBackgroundColor(FSlateColor(FLinearColor::White)) .Padding(0) [SNew(SBox) .MinDesiredHeight(26.0f) [RowWidget.ToSharedRef()]]]]]; } /** * Build the filter section layout */ void SOculusXRProjectSetupToolWidget::BuildFilterSectionLayout(const TSharedPtr& RootContainer, const uint32 PlatformFilterIndex) { // Top level container for the whole row const TSharedPtr RowWidget = SNew(SHorizontalBox); uint32 index = 0; for (const auto& Platform : PlatformFilters) { const FSlateBrush* BorderBrush = FAppStyle::Get().GetBrush("DetailsView.GridLine"); if (index == PlatformFilterIndex) { BorderBrush = FAppStyle::Get().GetBrush("DetailsView.CategoryTop"); } ++index; RowWidget->AddSlot() .Padding(FMargin(0, 1, 1, 1)) [SNew(SBorder) .BorderImage(BorderBrush) [SNew(SHorizontalBox) + SHorizontalBox::Slot() [SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ContentPadding(0) .ButtonStyle(FCoreStyle::Get(), "NoBorder") .OnClicked(this, &SOculusXRProjectSetupToolWidget::OnPlatformFilterChanged, Platform) [SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(0, 3, 3, 3) [SNew(SImage) .Image( FOculusXRProjectSetupToolModule::GetSlateStyle() ->GetBrush(OculusXRPSTUtils::GetDisplayName( Platform))) .ColorAndOpacity(FSlateColor::UseForeground())] + SHorizontalBox::Slot() .Padding(3, 0, 0, 0) .AutoWidth() .VAlign(VAlign_Center) [SNew(STextBlock) .Text(FText::FromString(OculusXRPSTUtils::GetDisplayName(Platform)))]]]]]; } // Add into the root container RootContainer->AddSlot() .AutoHeight() .Padding(0) [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.GridLine")) .Padding(FMargin(0, 0, 0, 1)) [SNew(SHorizontalBox) + SHorizontalBox::Slot() [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop")) .BorderBackgroundColor(FSlateColor(FLinearColor::White)) .Padding(0) [SNew(SBox) .MinDesiredHeight(26.0f) [RowWidget.ToSharedRef()]]]]]; } /** * Build a container for a rules section */ TSharedPtr SOculusXRProjectSetupToolWidget::BuildRulesContainerLayout(const TSharedPtr& RootContainer, ERulesSection Section, const FText& SectionTitle) { // Top level container for the whole row const TSharedPtr TitleAndExpanderWidget = SNew(SHorizontalBox) .Clipping(EWidgetClipping::OnDemand); TitleAndExpanderWidget->AddSlot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(8, 0, 8, 0) .AutoWidth() [SNew(SHorizontalBox) .Visibility_Static(&SOculusXRProjectSetupToolWidget::OnHeaderExpanderVisibility, Section) + SHorizontalBox::Slot() [SNew(SButton) .ButtonStyle(FCoreStyle::Get(), "NoBorder") .VAlign(VAlign_Center) .HAlign(HAlign_Center) .ClickMethod(EButtonClickMethod::MouseDown) .OnClicked(this, &SOculusXRProjectSetupToolWidget::OnHeaderExpanderClicked, Section) .ContentPadding(0) .IsFocusable(false) [SNew(SImage) .Image(this, &SOculusXRProjectSetupToolWidget::GetHeaderExpanderImage, Section) .ColorAndOpacity(FSlateColor::UseSubduedForeground())]]]; TitleAndExpanderWidget->AddSlot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(0, 0, 0, 0) [SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle(TEXT("DetailsView.CategoryFontStyle"))) .TextStyle(FAppStyle::Get(), "DetailsView.CategoryTextStyle") .Text(SectionTitle)]; const TSharedPtr EmptyDescriptionWidget = SNew(SHorizontalBox); const TSharedPtr ButtonWidget = SNew(SHorizontalBox); if (Section == ERulesSection::Required || Section == ERulesSection::Recommended) { ButtonWidget->AddSlot() .Padding(FMargin(6, 3, 3, 3)) [SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("PrimaryButton")) .Text(LOCTEXT("ApplyAllRules", "Apply All")) .OnClicked(this, &SOculusXRProjectSetupToolWidget::OnApplyAllRulesClicked, Section) .IsEnabled_Raw(this, &SOculusXRProjectSetupToolWidget::OnApplyAllRulesEnabled, Section)]; // This is not actually needed but we add it to ensure correct alignment ButtonWidget->AddSlot() .AutoWidth() .VAlign(VAlign_Center) .Padding(FMargin(4.f, 0.f, 8.f, 0.f)) [SNew(SComboButton) .ContentPadding(0) .Visibility_Lambda([this]() -> EVisibility { return EVisibility::Hidden; }) .HasDownArrow(false) .ForegroundColor(FSlateColor::UseForeground()) .ComboButtonStyle(FAppStyle::Get(), "SimpleComboButton") .ButtonContent() [SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Center) [SNew(SImage) .Image(FAppStyle::Get().GetBrush("PropertyWindow.Button_Ellipsis")) .ColorAndOpacity(FSlateColor::UseForeground())]]]; } // Splitter so each row can contain Name | Description | Buttons const TSharedPtr Splitter = SNew(SSplitter) .Style(FAppStyle::Get(), "DetailsView.Splitter") .PhysicalSplitterHandleSize(1.0f) .HitDetectionSplitterHandleSize(5.0f); // Add the widgets to the splitter Splitter->AddSlot() .Value(ColumnSizeData->GetNameColumnWidth()) .OnSlotResized(ColumnSizeData->GetOnNameColumnResized()) [TitleAndExpanderWidget.ToSharedRef()]; Splitter->AddSlot() .Value(ColumnSizeData->GetValueColumnWidth()) .OnSlotResized(ColumnSizeData->GetOnValueColumnResized()) [EmptyDescriptionWidget.ToSharedRef()]; Splitter->AddSlot() .Value(ColumnSizeData->GetRightColumnWidth()) .OnSlotResized(ColumnSizeData->GetOnRightColumnResized()) .MinSize(ColumnSizeData->GetRightColumnMinWidth()) [ButtonWidget.ToSharedRef()]; // Top level container for the whole row const TSharedPtr RowWidget = SNew(SBox) .Padding(0) [Splitter.ToSharedRef()]; // Add into the root container RootContainer->AddSlot() .Padding(0) [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.GridLine")) .Padding(FMargin(0, 0, 0, 1)) [SNew(SHorizontalBox) + SHorizontalBox::Slot() [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryTop")) .BorderBackgroundColor(FSlateColor(FLinearColor::White)) .Padding(0) [SNew(SBox) .MinDesiredHeight(26.0f) [RowWidget.ToSharedRef()]]]]]; // Contents TSharedPtr SectionContentContainer = SNew(SVerticalBox); RootContainer->AddSlot() .Padding(0) [SectionContentContainer.ToSharedRef()]; return SectionContentContainer; } /** * Build the layout for a single row (filter or rule row) */ void SOculusXRProjectSetupToolWidget::BuildRowItemLayout(const TSharedPtr& SectionContentContainer, ERulesSection Section, const SetupRulePtr& Rule, const uint32 PlatformFilterIndex) { // RowWidget -> Splitter -> (NameWidget | DescriptionWidget | ButtonWidget) const bool bShouldHideApplyButton = Section == ERulesSection::Applied; const FSlateBrush* IconBrush = nullptr; if (Rule != nullptr) { if (Section == ERulesSection::Applied) { IconBrush = FAppStyle::Get().GetBrush("Icons.SuccessWithColor"); } else if (Rule->GetSeverity() == ESetupRuleSeverity::Critical) { IconBrush = FAppStyle::Get().GetBrush("Icons.ErrorWithColor"); } else { IconBrush = FAppStyle::Get().GetBrush("Icons.WarningWithColor"); } } // Name widget const TSharedPtr NameWidget = SNew(SHorizontalBox) .Clipping(EWidgetClipping::OnDemand); if (IconBrush != nullptr) { NameWidget->AddSlot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(8, 0, 8, 0) .AutoWidth() [SNew(SImage) .Image(IconBrush)]; } NameWidget->AddSlot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(0, 0, 0, 0) [SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) .ColorAndOpacity(FSlateColor::UseStyle()) .Text(Rule != nullptr ? Rule->GetDisplayName() : FText::GetEmpty())]; // Description widget const TSharedPtr DescriptionWidget = SNew(SHorizontalBox) .Clipping(EWidgetClipping::OnDemand); DescriptionWidget->AddSlot() .VAlign(VAlign_Center) .Padding(6, 0) [SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle(TEXT("PropertyWindow.NormalFont"))) .ColorAndOpacity(FSlateColor::UseStyle()) .Text(Rule != nullptr ? Rule->GetDescription() : FText::GetEmpty())]; // Button widget. There are two buttons slots that are customised based on section const TSharedPtr ButtonWidget = SNew(SHorizontalBox); ButtonWidget->AddSlot() .Padding(FMargin(6, 3, 3, 3)) [SNew(SButton) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Text(LOCTEXT("ApplyRule", "Apply")) .Visibility_Lambda([bShouldHideApplyButton]() -> EVisibility { return bShouldHideApplyButton ? EVisibility::Hidden : EVisibility::Visible; }) .OnClicked(this, &SOculusXRProjectSetupToolWidget::OnApplyRuleClicked, Rule) .IsEnabled_Static(&SOculusXRProjectSetupToolWidget::OnApplyRuleEnabled, Section)]; FMenuBuilder EllipsisMenuBuilder(true, nullptr); const auto IgnoreButtonText = Section == ERulesSection::Ignored ? LOCTEXT("UnignoreRule", "Unignore") : LOCTEXT("IgnoreRule", "Ignore"); const auto IgnoreButtonTooltip = Section == ERulesSection::Ignored ? LOCTEXT("UnignoreRule_Tooltip", "Unignore this rule") : LOCTEXT("IgnoreRule_Tooltip", "Ignore this rule"); const FString SupportURL(TEXT("https://forums.oculusvr.com/developer")); EllipsisMenuBuilder.AddMenuEntry( IgnoreButtonText, IgnoreButtonTooltip, FSlateIcon(), FUIAction( FExecuteAction::CreateRaw(this, &SOculusXRProjectSetupToolWidget::OnIgnoreRuleClicked, Rule, Section), FCanExecuteAction::CreateStatic(&SOculusXRProjectSetupToolWidget::OnIgnoreRuleEnabled, Section))); EllipsisMenuBuilder.AddMenuEntry( LOCTEXT("SupportButton", "Support"), LOCTEXT("SupportButton_ToolTip", "Get more information about this rule."), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Comment"), FUIAction( FExecuteAction::CreateLambda([SupportURL] { FPlatformProcess::LaunchURL(*SupportURL, nullptr, nullptr); }), FCanExecuteAction())); ButtonWidget->AddSlot() .AutoWidth() .VAlign(VAlign_Center) .Padding(FMargin(4.f, 0.f, 8.f, 0.f)) [SNew(SComboButton) .ContentPadding(0) .HasDownArrow(false) .Visibility_Lambda([bShouldHideApplyButton]() -> EVisibility { return bShouldHideApplyButton ? EVisibility::Hidden : EVisibility::Visible; }) .ForegroundColor(FSlateColor::UseForeground()) .ComboButtonStyle(FAppStyle::Get(), "SimpleComboButton") .AddMetaData(FTagMetaData(TEXT("ViewOptions"))) .MenuContent() [EllipsisMenuBuilder.MakeWidget()] .ButtonContent() [SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Center) [SNew(SImage) .Image(FAppStyle::Get().GetBrush("PropertyWindow.Button_Ellipsis")) .ColorAndOpacity(FSlateColor::UseForeground())]]]; // Splitter so each row can contain Name | Description | Buttons const TSharedPtr Splitter = SNew(SSplitter) .Style(FAppStyle::Get(), "DetailsView.Splitter") .PhysicalSplitterHandleSize(1.0f) .HitDetectionSplitterHandleSize(5.0f); // Add the widgets to the splitter Splitter->AddSlot() .Value(ColumnSizeData->GetNameColumnWidth()) .OnSlotResized(ColumnSizeData->GetOnNameColumnResized()) [NameWidget.ToSharedRef()]; Splitter->AddSlot() .Value(ColumnSizeData->GetValueColumnWidth()) .OnSlotResized(ColumnSizeData->GetOnValueColumnResized()) [DescriptionWidget.ToSharedRef()]; Splitter->AddSlot() .Value(ColumnSizeData->GetRightColumnWidth()) .OnSlotResized(ColumnSizeData->GetOnRightColumnResized()) .MinSize(ColumnSizeData->GetRightColumnMinWidth()) [ButtonWidget.ToSharedRef()]; // Top level container for the whole row const TSharedPtr RowWidget = SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) .Padding(0) [Splitter.ToSharedRef()]; // Add a slot into the content container for this item and add the row widget to it SectionContentContainer->AddSlot() .AutoHeight() [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.GridLine")) .Padding(FMargin(0, 0, 0, 1)) .Clipping(EWidgetClipping::ClipToBounds) .Visibility(this, &SOculusXRProjectSetupToolWidget::OnRowVisibility, Section, PlatformFilters[CurrentPlatformFilterIndex], Rule) [SNew(SBox) .MinDesiredHeight(26.0f) [SNew(SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Fill) [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.Highlight")) .Padding(0) [SNew(SBorder) .BorderImage(FAppStyle::Get().GetBrush("DetailsView.CategoryMiddle")) .BorderBackgroundColor(FAppStyle::Get().GetSlateColor("Colors.Panel")) .Padding(0) [RowWidget.ToSharedRef()]]]]]]; } /** * Build the layout for the required rules */ void SOculusXRProjectSetupToolWidget::BuildRequiredRulesSectionLayout(const TSharedPtr& RootContainer) { const UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); if (RuleProcessorSubsystem == nullptr) { return; } const TSharedPtr SectionContentContainer = BuildRulesContainerLayout(RootContainer, ERulesSection::Required, LOCTEXT("RequiredRules_Title", "Required Rules")); for (const auto& Rule : RuleProcessorSubsystem->GetRules()) { if (Rule->GetSeverity() == ESetupRuleSeverity::Critical) { BuildRowItemLayout(SectionContentContainer, ERulesSection::Required, Rule, 0); } } } /** * Build the layout for the recommended rules */ void SOculusXRProjectSetupToolWidget::BuildRecommendedRulesSectionLayout(const TSharedPtr& RootContainer) { const UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); if (RuleProcessorSubsystem == nullptr) { return; } const TSharedPtr SectionContentContainer = BuildRulesContainerLayout(RootContainer, ERulesSection::Recommended, LOCTEXT("RecommendedRules_Title", "Recommended Rules")); for (const auto& Rule : RuleProcessorSubsystem->GetRules()) { if (Rule->GetSeverity() < ESetupRuleSeverity::Critical) { BuildRowItemLayout(SectionContentContainer, ERulesSection::Recommended, Rule, 0); } } } /** * Build the layout for the applied rules */ void SOculusXRProjectSetupToolWidget::BuildAppliedRulesSectionLayout(const TSharedPtr& RootContainer) { const UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); if (RuleProcessorSubsystem == nullptr) { return; } const TSharedPtr SectionContentContainer = BuildRulesContainerLayout(RootContainer, ERulesSection::Applied, LOCTEXT("AppliedRules_Title", "Applied Rules")); // Add all rules and let the visibility function take care of which ones to show for (const auto& Rule : RuleProcessorSubsystem->GetRules()) { BuildRowItemLayout(SectionContentContainer, ERulesSection::Applied, Rule, 0); } } /** * Build the layout for the ignored rules */ void SOculusXRProjectSetupToolWidget::BuildIgnoredRulesSectionLayout(const TSharedPtr& RootContainer) { const UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); if (RuleProcessorSubsystem == nullptr) { return; } const TSharedPtr SectionContentContainer = BuildRulesContainerLayout(RootContainer, ERulesSection::Ignored, LOCTEXT("IgnoredRules_Title", "Ignored Rules")); // Add all rules and let the visibility function take care of which ones to show for (const auto& Rule : RuleProcessorSubsystem->GetRules()) { BuildRowItemLayout(SectionContentContainer, ERulesSection::Ignored, Rule, 0); } } /** * Is an un-applied rule visible? */ EVisibility SOculusXRProjectSetupToolWidget::OnRowVisibility(ERulesSection Section, ESetupRulePlatform CurrentPlatform, SetupRulePtr Rule) const { if (Section == ERulesSection::Filter) { return EVisibility::Visible; } if (!BIsSectionExpanded[static_cast(Section)]) { return EVisibility::Collapsed; } if (Rule == nullptr) { return EVisibility::Collapsed; } if (!Rule->IsValid()) { return EVisibility::Collapsed; } if ((Rule->GetPlatform() & CurrentPlatform) != CurrentPlatform) { return EVisibility::Collapsed; } switch (Section) { case ERulesSection::Required: case ERulesSection::Recommended: { if (Rule->IsApplied()) { return EVisibility::Collapsed; } return Rule->IsIgnored() ? EVisibility::Collapsed : EVisibility::Visible; } case ERulesSection::Applied: { return Rule->IsApplied() ? EVisibility::Visible : EVisibility::Collapsed; } case ERulesSection::Ignored: { // Applied rules always show in the Applied section even if ignored if (Rule->IsApplied()) { return EVisibility::Collapsed; } return Rule->IsIgnored() ? EVisibility::Visible : EVisibility::Collapsed; } default: { break; } } return EVisibility::Collapsed; } EVisibility SOculusXRProjectSetupToolWidget::OnRestartEditorNoticeVisibility() const { return bShowButtonToRestart ? EVisibility::Visible : EVisibility::Collapsed; } FReply SOculusXRProjectSetupToolWidget::OnRestartEditorButtonClicked() { bShowButtonToRestart = false; FUnrealEdMisc::Get().RestartEditor(false); return FReply::Handled(); } void SOculusXRProjectSetupToolWidget::Refresh() { UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); if (RuleProcessorSubsystem != nullptr) { RuleProcessorSubsystem->Refresh(); } } void SOculusXRProjectSetupToolWidget::UpdateActiveTimer(bool Register) { if (!Register) { if (!ActiveTimerHandle.IsValid()) { return; } UnRegisterActiveTimer(ActiveTimerHandle.Pin().ToSharedRef()); return; } if (ActiveTimerHandle.IsValid()) { UnRegisterActiveTimer(ActiveTimerHandle.Pin().ToSharedRef()); } ActiveTimerHandle = RegisterActiveTimer( 30.f, FWidgetActiveTimerDelegate::CreateLambda([this](double /*InCurrentTime*/, float /*InDeltaTime*/) { Refresh(); UpdateProjectStatus(); return EActiveTimerReturnType::Continue; })); } /** * Apply a new platform filter */ FReply SOculusXRProjectSetupToolWidget::OnPlatformFilterChanged(ESetupRulePlatform ItemSelected) { const UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); if (RuleProcessorSubsystem == nullptr) { return FReply::Handled(); } const int32 Index = PlatformFilters.Find(ItemSelected); if (Index != INDEX_NONE) { CurrentPlatformFilterIndex = Index; GetMutableDefault()->CurrentPlatform = static_cast(ItemSelected); BuildLayout(RootContainerWidget); } return FReply::Handled(); } /** * Apply rule button callback */ FReply SOculusXRProjectSetupToolWidget::OnApplyRuleClicked(SetupRulePtr Rule) { if (Rule == nullptr) { return FReply::Handled(); } bool bShouldRestartEditor = false; Rule->Apply(bShouldRestartEditor); bShowButtonToRestart |= bShouldRestartEditor; UpdateProjectStatus(); return FReply::Handled(); } /** * Apply rule enabled callback */ bool SOculusXRProjectSetupToolWidget::OnApplyRuleEnabled(ERulesSection Section) { return Section != ERulesSection::Applied && Section != ERulesSection::Ignored; } /** * Ignore/Unignore rule button callback */ void SOculusXRProjectSetupToolWidget::OnIgnoreRuleClicked(SetupRulePtr Rule, ERulesSection Section) { if (Rule == nullptr) { return; } if (Section != ERulesSection::Ignored) { Rule->SetIgnoreRule(true); } else { Rule->SetIgnoreRule(false); } UpdateProjectStatus(); } /** * Apply rule button callback */ FReply SOculusXRProjectSetupToolWidget::OnApplyAllRulesClicked(ERulesSection Section) { bool bShouldRestartEditor = false; const UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); for (auto& Rule : RuleProcessorSubsystem->GetRules()) { // Only apply rules that are critical and are in the required section or non-critical and are in the recommended section bool bShouldApplyRule = Rule->GetSeverity() == ESetupRuleSeverity::Critical && Section == ERulesSection::Required; bShouldApplyRule = bShouldApplyRule || Rule->GetSeverity() != ESetupRuleSeverity::Critical && Section == ERulesSection::Recommended; // Only apply rules that in the current platform bShouldApplyRule = bShouldApplyRule && (Rule->GetPlatform() & PlatformFilters[CurrentPlatformFilterIndex]) == PlatformFilters[CurrentPlatformFilterIndex]; // Only apply rules that are valid bShouldApplyRule = bShouldApplyRule && Rule->IsValid(); // Only apply rules that are not applied yet bShouldApplyRule = bShouldApplyRule && !Rule->IsApplied(); // Only apply rules that are not ignored bShouldApplyRule = bShouldApplyRule && !Rule->IsIgnored(); if (!bShouldApplyRule) { continue; } Rule->Apply(bShouldRestartEditor); bShowButtonToRestart |= bShouldRestartEditor; } UpdateProjectStatus(); return FReply::Handled(); } /** * Apply rule enabled callback */ bool SOculusXRProjectSetupToolWidget::OnApplyAllRulesEnabled(ERulesSection Section) const { switch (Section) { case ERulesSection::Required: return RuleStatus.PendingRequiredRulesCount > 0; case ERulesSection::Recommended: return RuleStatus.PendingRecommendedRulesCount > 0; default: return false; } } /** * Ignore rule enabled callback */ bool SOculusXRProjectSetupToolWidget::OnIgnoreRuleEnabled(ERulesSection Section) { return Section != ERulesSection::Applied; } /** * Expander visibility */ EVisibility SOculusXRProjectSetupToolWidget::OnHeaderExpanderVisibility(ERulesSection Section) { return Section == ERulesSection::Filter ? EVisibility::Collapsed : EVisibility::Visible; } /** * Expander image */ const FSlateBrush* SOculusXRProjectSetupToolWidget::GetHeaderExpanderImage(ERulesSection Section) const { const bool bIsHeaderExpanded = BIsSectionExpanded[static_cast(Section)]; FName ResourceName; if (bIsHeaderExpanded) { static const FName ExpandedName = "TreeArrow_Expanded"; ResourceName = ExpandedName; } else { static const FName CollapsedName = "TreeArrow_Collapsed"; ResourceName = CollapsedName; } return FAppStyle::Get().GetBrush(ResourceName); } /** * Expander clicked */ FReply SOculusXRProjectSetupToolWidget::OnHeaderExpanderClicked(ERulesSection Section) { const uint8 index = static_cast(Section); BIsSectionExpanded[index] = !BIsSectionExpanded[index]; return FReply::Handled(); } void SOculusXRProjectSetupToolWidget::UpdateProjectStatus() { const UOculusXRRuleProcessorSubsystem* RuleProcessorSubsystem = GEngine->GetEngineSubsystem(); if (RuleProcessorSubsystem != nullptr) { RuleStatus = RuleProcessorSubsystem->UnAppliedRulesStatus(PlatformFilters[CurrentPlatformFilterIndex]); } UpdateProjectStatusString(); } /** * Build the project status string */ void SOculusXRProjectSetupToolWidget::UpdateProjectStatusString() const { if (ProjectStatusWidget == nullptr) { return; } FString Status; if (RuleStatus.PendingRequiredRulesCount == 0 && RuleStatus.PendingRecommendedRulesCount == 0) { Status = TEXT("Current Project Status: All rules have been applied"); } else if (RuleStatus.PendingRequiredRulesCount == 0) { Status = FString::Printf(TEXT("Current Project Status: There are %d recommended rules to apply"), RuleStatus.PendingRecommendedRulesCount); } else if (RuleStatus.PendingRecommendedRulesCount == 0) { Status = FString::Printf(TEXT("Current Project Status: There are %d required rules to apply"), RuleStatus.PendingRequiredRulesCount); } else { Status = FString::Printf(TEXT("Current Project Status: There are %d required rules and %d recommended rules to apply"), RuleStatus.PendingRequiredRulesCount, RuleStatus.PendingRecommendedRulesCount); } ProjectStatusWidget->SetText(FText::FromString(Status)); } #undef LOCTEXT_NAMESPACE