Files
2025-05-18 13:04:45 +08:00

309 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Extensions/MVVMViewBlueprintPanelWidgetExtension.h"
#include "Bindings/MVVMCompiledBindingLibraryCompiler.h"
#include "Blueprint/WidgetBlueprintGeneratedClass.h"
#include "Components/PanelWidget.h"
#include "Extensions/MVVMViewPanelWidgetExtension.h"
#include "MVVMBlueprintView.h"
#include "MVVMBlueprintViewCompilerInterface.h"
#include "MVVMBlueprintViewModelContext.h"
#include "MVVMPropertyPath.h"
#include "MVVMWidgetBlueprintExtension_View.h"
#include "Slate/SObjectWidget.h"
#include "Templates/ValueOrError.h"
#include "UObject/UnrealType.h"
#include "View/MVVMViewClass.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(MVVMViewBlueprintPanelWidgetExtension)
#define LOCTEXT_NAMESPACE "MVVMViewBlueprintPanelWidgetExtension"
TArray<UE::MVVM::Compiler::FBlueprintViewUserWidgetProperty> UMVVMBlueprintViewExtension_PanelWidget::AddProperties()
{
TArray<UE::MVVM::Compiler::FBlueprintViewUserWidgetProperty> PropertiesToAdd;
if (!WidgetName.IsNone())
{
// Add the runtime panel widget extension as a variable
PanelPropertyName = FName(FString::Printf(TEXT("%s_%s"), *WidgetName.ToString(), *FString(TEXT("Viewmodel_Extension"))));
UE::MVVM::Compiler::FBlueprintViewUserWidgetProperty Property;
Property.AuthoritativeClass = UMVVMPanelWidgetViewExtension::StaticClass();
Property.DisplayName = FText::FromName(PanelPropertyName);
Property.Name = PanelPropertyName;
Property.CategoryName = TEXT("PanelWidgetExtension");
Property.bReadOnly = true;
PropertiesToAdd.Add(Property);
}
return PropertiesToAdd;
}
TArray<UE::MVVM::Compiler::FBlueprintViewUserWidgetWidgetProperty> UMVVMBlueprintViewExtension_PanelWidget::AddWidgetProperties()
{
TArray<UE::MVVM::Compiler::FBlueprintViewUserWidgetWidgetProperty> WidgetPropertiesToAdd;
if (!WidgetName.IsNone())
{
// Add the panel widget as a variable
UE::MVVM::Compiler::FBlueprintViewUserWidgetWidgetProperty WidgetProperty;
WidgetProperty.WidgetName = WidgetName;
WidgetPropertiesToAdd.Add(WidgetProperty);
}
return WidgetPropertiesToAdd;
}
void UMVVMBlueprintViewExtension_PanelWidget::Precompile(UE::MVVM::Compiler::IMVVMBlueprintViewPrecompile* Compiler, UWidgetBlueprintGeneratedClass* Class)
{
check (Compiler);
auto VerifyViewmodelTypeMatch = [this, Compiler](const TSubclassOf<UUserWidget> InEntryWidgetClass, const FMVVMBlueprintPropertyPath& EntryViewModelPath, const FName& PropertyName)
{
bool bFoundSetterBinding = false;
// Find bindings that have write fields with metadata "ViewmodelBlueprintWidgetExtension", these are setter functions for assigning a viewmodel array.
// Use their corresponding read field to get the expected type of entry viewmodel, and compare it against the one that user selected in the details panel.
for (const UE::MVVM::Compiler::FCompilerBindingHandle& Binding : Compiler->GetAllBindings())
{
TArray<UE::MVVM::FMVVMConstFieldVariant> WriteFields = Compiler->GetBindingWriteFields(Binding);
for (int32 FieldIndex = 1; FieldIndex < WriteFields.Num(); FieldIndex++)
{
UE::MVVM::FMVVMConstFieldVariant& PathField = WriteFields[FieldIndex];
UE::MVVM::FMVVMConstFieldVariant& ParentPathField = WriteFields[FieldIndex - 1];
if (PathField.IsFunction() && ParentPathField.IsProperty() && PropertyName == ParentPathField.GetName())
{
const FString MetaData = PathField.GetFunction()->GetMetaData("ViewmodelBlueprintWidgetExtension");
if (MetaData.Equals("EntryViewmodel", ESearchCase::IgnoreCase))
{
if (const FProperty* SourceProperty = Compiler->GetBindingSourceProperty(Binding))
{
if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(SourceProperty))
{
// This is the viewmodel that the user selected from the details panel
TArray<UE::MVVM::FMVVMConstFieldVariant> SelectedVMFields = EntryViewModelPath.GetFields(InEntryWidgetClass);
if (SelectedVMFields.Num() > 0)
{
UE::MVVM::FMVVMConstFieldVariant SelectedVMPropertyField = SelectedVMFields.Last();
if (SelectedVMPropertyField.IsProperty())
{
bFoundSetterBinding = true;
const FProperty* SelectedVMProperty = SelectedVMPropertyField.GetProperty();
if (!ArrayProperty->Inner->SameType(SelectedVMProperty))
{
const FText UserSelectedViewmodelType = FText::FromString(SelectedVMProperty->GetCPPType());
const FText ArraySetterFunctionViewmodelType = FText::FromString(ArrayProperty->Inner->GetCPPType());
Compiler->AddMessageForBinding(Binding, FText::Format(LOCTEXT("EntryViewModelTypeMismatch", "The entry viewmodel type {0} does not match the array of viewmodels of type {1}."), UserSelectedViewmodelType, ArraySetterFunctionViewmodelType), UE::MVVM::Compiler::EMessageType::Error);
Compiler->MarkPrecompileStepInvalid();
break;
}
}
}
}
}
// No need to look into the next fields of this write path, we have found the field with ViewmodelBlueprintWidgetExtension metadata.
// We will go to the next binding
break;
}
}
}
}
if (!bFoundSetterBinding && EntryViewModelId.IsValid())
{
Compiler->AddMessage(FText::Format(LOCTEXT("PreCompileMVVMWidgetExtensionNoSetterBindingFound", "No binding found from an array of viewmodels to {0} -> SetItems. Please find {0} on the root widget and add this binding or remove the Viewmodel extension on widget {1} from its details panel."), FText::FromName(PanelPropertyName), FText::FromName(WidgetName)), UE::MVVM::Compiler::EMessageType::Warning);
}
};
if (UWidget* const* FoundPanel = Compiler->GetWidgetNameToWidgetPointerMap().Find(WidgetName))
{
if (UPanelWidget* PanelWidget = Cast<UPanelWidget>(*FoundPanel))
{
if (EntryWidgetClass.Get())
{
UE::MVVM::Compiler::IMVVMBlueprintViewPrecompile::FObjectFieldPathArgs AddObjFieldPathArgs(Class, WidgetName.ToString(), UPanelWidget::StaticClass());
TValueOrError<UE::MVVM::FCompiledBindingLibraryCompiler::FFieldPathHandle, FText> FieldPathResult = Compiler->AddObjectFieldPath(AddObjFieldPathArgs);
if (FieldPathResult.HasError() || !FieldPathResult.GetValue().IsValid())
{
Compiler->AddMessage(FText::Format(LOCTEXT("CouldNotCreateSourceFieldPath", "Couldn't create the source field path '{0}'. {1}. Make sure '{0}' is marked as 'Is Variable'."), FText::FromName(WidgetName), FieldPathResult.GetError()), UE::MVVM::Compiler::EMessageType::Error);
Compiler->MarkPrecompileStepInvalid();
return;
}
WidgetPathHandle = FieldPathResult.StealValue();
if (const UUserWidget* EntryUserWidget = Cast<UUserWidget>(EntryWidgetClass->GetDefaultObject(false)))
{
if (const UMVVMBlueprintView* EntryBPView = GetEntryWidgetBlueprintView(EntryUserWidget))
{
if (const FMVVMBlueprintViewModelContext* ViewModelContext = EntryBPView->FindViewModel(EntryViewModelId))
{
const FName EntryViewModelName = ViewModelContext->GetViewModelName();
if (const UWidgetBlueprint* EntryBlueprint = Cast<UWidgetBlueprint>(EntryUserWidget->GetClass()->ClassGeneratedBy))
{
// find the property path of the entry viewmodel
FMVVMBlueprintPropertyPath SelectedViewModel;
SelectedViewModel.SetViewModelId(EntryViewModelId);
FProperty* ViewModelProperty = FindFProperty<FProperty>(*EntryWidgetClass, EntryViewModelName);
SelectedViewModel.AppendPropertyPath(EntryBlueprint, UE::MVVM::FMVVMConstFieldVariant(ViewModelProperty));
VerifyViewmodelTypeMatch(EntryWidgetClass, SelectedViewModel, PanelPropertyName);
}
}
else
{
// If the stored viewmodel ID invalid, it means the viewmodel was deleted so we clear the value.
// We need to manually clear this value here because the panel widget doesn't get notified when the entry widget viewmodels change.
EntryViewModelId = FGuid();
Compiler->AddMessage(FText::Format(LOCTEXT("PreCompileMVVMWidgetExtensionEntryVMDeleted", "No viewmodel selected for Entry widget {0}. Please select a viewmodel for it via the details panel or remove the Viewmodel extension on the containing widget {1}."), FText::FromName(EntryUserWidget->GetFName()), FText::FromName(WidgetName)), UE::MVVM::Compiler::EMessageType::Error);
}
}
else
{
// If no view is found in the entry widget, we clear the entry viewmodel value.
// We need to manually clear this value here because the panel widget doesn't get notified when the entry widget view is added/removed.
EntryViewModelId = FGuid();
Compiler->AddMessage(FText::Format(LOCTEXT("PreCompileMVVMWidgetExtensionEntryHasNoView", "Entry widget {0} doesn't have a View. Consider adding a binding to it or remove the MVVM extension on the containing widget {1}."), FText::FromName(EntryUserWidget->GetFName()), FText::FromName(WidgetName)), UE::MVVM::Compiler::EMessageType::Error);
}
}
}
else
{
Compiler->AddMessage(FText::Format(LOCTEXT("PreCompileMVVMWidgetExtensionWidgetNoEntryClass", "Widget {0} doesn't have an entry widget class. Consider assigning it in the details panel or remove the Viewmodel extension on the widget."), FText::FromName(WidgetName)), UE::MVVM::Compiler::EMessageType::Error);
}
}
else
{
Compiler->AddMessage(FText::Format(LOCTEXT("PreCompileMVVMWidgetExtensionWidgetNotPanelWidget", "Widget {0} is not a UPanelWidget but has a MVVMViewBlueprintPanelWidgetExtension."), FText::FromName(WidgetName)), UE::MVVM::Compiler::EMessageType::Error);
}
}
else
{
Compiler->AddMessage(FText::Format(LOCTEXT("PreCompileMVVMWidgetExtensionInvalidWidgetName", "Widget with name {0} doesn't exist in the widget blueprint but a viewmodel widget extension exists that is attached to it."), FText::FromName(WidgetName)),UE::MVVM::Compiler::EMessageType::Error);
}
}
void UMVVMBlueprintViewExtension_PanelWidget::Compile(UE::MVVM::Compiler::IMVVMBlueprintViewCompile* Compiler, UWidgetBlueprintGeneratedClass* Class, UMVVMViewClass* ViewExtension)
{
check(Compiler);
if (UWidget* const* FoundPanel = Compiler->GetWidgetNameToWidgetPointerMap().Find(WidgetName))
{
if (UPanelWidget* PanelWidget = Cast<UPanelWidget>(*FoundPanel))
{
if (EntryWidgetClass.Get())
{
check(WidgetPathHandle.IsValid());
// Verify the widget property
const TValueOrError<FMVVMVCompiledFieldPath, void> CompiledFieldPath = Compiler->GetFieldPath(WidgetPathHandle);
if (CompiledFieldPath.HasError())
{
Compiler->AddMessage(FText::Format(LOCTEXT("CompiledFieldPathForWidgetNotGenerated", "The field path for widget {0} was not generated."), FText::FromName(WidgetName)), UE::MVVM::Compiler::EMessageType::Error);
Compiler->MarkCompileStepInvalid();
return;
}
// Check that we have a valid entry view and valid entry viewmodel
// Otherwise, no runtime extension will be created.
if (const UUserWidget* EntryUserWidget = Cast<UUserWidget>(EntryWidgetClass->GetDefaultObject(false)))
{
if (const UMVVMBlueprintView* EntryBPView = GetEntryWidgetBlueprintView(EntryUserWidget))
{
if (const FMVVMBlueprintViewModelContext* ViewModelContext = EntryBPView->FindViewModel(EntryViewModelId))
{
// Create the corresponding runtime extension
UMVVMViewClassExtension* NewExtensionObj = Compiler->CreateViewClassExtension(UMVVMViewPanelWidgetClassExtension::StaticClass());
UMVVMViewPanelWidgetClassExtension* NewExtension = CastChecked<UMVVMViewPanelWidgetClassExtension>(NewExtensionObj);
const FName EntryViewModelName = ViewModelContext->GetViewModelName();
NewExtension->Initialize(UMVVMViewPanelWidgetClassExtension::FInitPanelWidgetExtensionArgs(WidgetName, EntryViewModelName, CompiledFieldPath.GetValue(), EntryWidgetClass, SlotObj, PanelPropertyName, ViewModelContext->NotifyFieldValueClass));
}
}
}
}
}
}
}
const UMVVMBlueprintView* UMVVMBlueprintViewExtension_PanelWidget::GetEntryWidgetBlueprintView(const UUserWidget* EntryUserWidget) const
{
if (const UWidgetBlueprint* EntryBlueprint = Cast<UWidgetBlueprint>(EntryUserWidget->GetClass()->ClassGeneratedBy))
{
if (const UMVVMWidgetBlueprintExtension_View* EntryWidgetExtension = UMVVMWidgetBlueprintExtension_View::GetExtension<UMVVMWidgetBlueprintExtension_View>(EntryBlueprint))
{
if (const UMVVMBlueprintView* EntryWidgetView = EntryWidgetExtension->GetBlueprintView())
{
return EntryWidgetView;
}
}
}
return nullptr;
}
void UMVVMBlueprintViewExtension_PanelWidget::RefreshDesignerPreviewEntries(UPanelWidget* PanelWidget, TSubclassOf<UUserWidget> EntryWidgetClass, UPanelSlot* SlotTemplate, int32 NumDesignerPreviewEntries, bool bFullRebuild)
{
if (ensure(PanelWidget))
{
if (bFullRebuild || !EntryWidgetClass)
{
PanelWidget->ClearChildren();
if (EntryWidgetClass)
{
for (int32 EntryIndex = 0; EntryIndex < NumDesignerPreviewEntries; ++EntryIndex)
{
if (UUserWidget* EntryWidget = UUserWidget::CreateWidgetInstance(*PanelWidget, EntryWidgetClass, NAME_None))
{
PanelWidget->AddChild(EntryWidget, SlotTemplate);
}
}
}
}
else if (NumDesignerPreviewEntries > PanelWidget->GetChildrenCount())
{
for (int32 NumToAdd = NumDesignerPreviewEntries - PanelWidget->GetChildrenCount(); NumToAdd > 0; --NumToAdd)
{
PanelWidget->AddChild(UUserWidget::CreateWidgetInstance(*PanelWidget, EntryWidgetClass, NAME_None), SlotTemplate);
}
}
else if (ensure(NumDesignerPreviewEntries >= 0) && NumDesignerPreviewEntries < PanelWidget->GetChildrenCount())
{
for (int32 NumToRemove = PanelWidget->GetChildrenCount() - NumDesignerPreviewEntries; NumToRemove > 0; --NumToRemove)
{
PanelWidget->RemoveChildAt(PanelWidget->GetChildrenCount() - 1);
}
}
}
}
bool UMVVMBlueprintViewExtension_PanelWidget::WidgetRenamed(FName OldName, FName NewName)
{
if (WidgetName == OldName)
{
Modify();
WidgetName = NewName;
return true;
}
return false;
}
void UMVVMBlueprintViewExtension_PanelWidget::OnPreviewContentChanged(TSharedRef<SWidget> NewContent)
{
if (NewContent != SNullWidget::NullWidget)
{
const SObjectWidget* ObjectWidget = StaticCastSharedPtr<SObjectWidget>(NewContent.ToSharedPtr()).Get();
const UUserWidget* PreviewRoot = ObjectWidget ? ObjectWidget->GetWidgetObject() : nullptr;
if (UPanelWidget* PreviewWidget = PreviewRoot ? Cast<UPanelWidget>(PreviewRoot->GetWidgetFromName(WidgetName)) : nullptr)
{
constexpr bool bFullRebuild = false;
RefreshDesignerPreviewEntries(PreviewWidget, EntryWidgetClass, SlotObj, NumDesignerPreviewEntries, bFullRebuild);
}
}
}
#undef LOCTEXT_NAMESPACE