// Copyright Epic Games, Inc. All Rights Reserved. #include "LiveLinkComponentDetailCustomization.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "LiveLinkComponentController.h" #include "LiveLinkControllerBase.h" #include "LiveLinkEditorPrivate.h" #include "ScopedTransaction.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SComboButton.h" #define LOCTEXT_NAMESPACE "LiveLinkComponentDetailsCustomization" void FLiveLinkComponentDetailCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { DetailLayout = &DetailBuilder; TArray> SelectedObjects = DetailBuilder.GetSelectedObjects(); //Hide everything when more than one are selected if (SelectedObjects.Num() != 1) { for (TWeakObjectPtr& SelectedObject : SelectedObjects) { if (ULiveLinkComponentController* SelectedPtr = Cast(SelectedObject.Get())) { TSharedRef ControllersProperty = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(ULiveLinkComponentController, ControllerMap)); ControllersProperty->MarkHiddenByCustomization(); } } return; } if (ULiveLinkComponentController* SelectedPtr = Cast(SelectedObjects[0].Get())) { // Force "LiveLink" to be the topmost category in the details panel DetailBuilder.EditCategory(TEXT("LiveLink")); //Hide the Map default UI TSharedRef ControllersProperty = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(ULiveLinkComponentController, ControllerMap)); ControllersProperty->MarkHiddenByCustomization(); //Get hook to the controller map. If that fails, early exit TSharedPtr MapHandle = ControllersProperty->AsMap(); if (MapHandle.IsValid()) { EditedObject = SelectedPtr; //Register callback when LiveLinkSubjectRepresentation selection has changed to refresh the UI and update controller TSharedRef SubjectRepresentationPropertyRef = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(ULiveLinkComponentController, SubjectRepresentation)); SubjectRepresentationPropertyRef->SetOnPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FLiveLinkComponentDetailCustomization::OnSubjectRepresentationPropertyChanged)); SubjectRepresentationPropertyRef->SetOnChildPropertyValueChanged(FSimpleDelegate::CreateSP(this, &FLiveLinkComponentDetailCustomization::OnSubjectRepresentationPropertyChanged)); //Listen to controller map modifications to refresh UI when a change comes through MU FSimpleDelegate RefreshDelegate = FSimpleDelegate::CreateSP(this, &FLiveLinkComponentDetailCustomization::ForceRefreshDetails); MapHandle->SetOnNumElementsChanged(RefreshDelegate); //Loop for each entry in the map. //Fetch the LiveLinkRole name (key) and display its name //Add a menu to select a ControllerClass for it //If a Controller is picked, display its properties uint32 NumEntry = 0; ControllersProperty->GetNumChildren(NumEntry); for (uint32 EntryIndex = 0; EntryIndex < NumEntry; ++EntryIndex) { TSharedPtr EntryHandle = ControllersProperty->GetChildHandle(EntryIndex); if (!EntryHandle.IsValid()) { continue; } TSharedPtr KeyHandle = EntryHandle->GetKeyHandle(); //Map has a TSubClassof Key type. Make sure we were able to query the UClass and that it's not null. UObject* LiveLinkRoleClassObj = nullptr; KeyHandle->GetValue(LiveLinkRoleClassObj); TSubclassOf LiveLinkRoleClass = Cast(LiveLinkRoleClassObj); if (LiveLinkRoleClass == nullptr) { continue; } FText ControllerName = FText::Format(LOCTEXT("No Controller", "{0}"), FText::FromName(NAME_None)); //Map value is a pointer to the Controller. Can be null so it could be empty TArray ExternalObjects; UObject* ControllerPtr = nullptr; EntryHandle->GetValue(ControllerPtr); if (ControllerPtr != nullptr) { ControllerName = ControllerPtr->GetClass()->GetDisplayNameText(); ExternalObjects.Add(ControllerPtr); } //Since we're displaying properties of another object, add it as external to the current one being edited. IDetailPropertyRow* ThisRoleRow = DetailBuilder.EditCategory(TEXT("Role Controllers")).AddExternalObjects(ExternalObjects, EPropertyLocation::Type::Default, FAddPropertyParams().UniqueId("LiveLinkControllerRow")); //As external objects, each map entry will generate a custom widget for its detail property row like this: //(NameWidget) | (ValueWidget) //Role Name | Dropdown menu with available controllers if (ThisRoleRow) { ThisRoleRow->CustomWidget() .NameContent() [ BuildControllerNameWidget(ControllersProperty, LiveLinkRoleClass) ] .ValueContent() [ BuildControllerValueWidget(LiveLinkRoleClass, ControllerName) ]; } } // Start by looking if data is dirty as we enter. Can happen when component lives in a blueprint OnSubjectRepresentationPropertyChanged(); } } } void FLiveLinkComponentDetailCustomization::OnSubjectRepresentationPropertyChanged() { if (ULiveLinkComponentController* EditedObjectPtr = EditedObject.Get()) { //Verify if Role has changed if (EditedObjectPtr->IsControllerMapOutdated()) { const FScopedTransaction Transaction(LOCTEXT("OnChangedSubjectRepresentation", "Subject Representation Changed")); EditedObjectPtr->Modify(); EditedObjectPtr->OnSubjectRoleChanged(); } } } TSharedRef FLiveLinkComponentDetailCustomization::HandleControllerComboButton(TSubclassOf RoleClass) const { // Generate menu FMenuBuilder MenuBuilder(true, nullptr); MenuBuilder.BeginSection("SupportedControllers", LOCTEXT("SupportedControllers", "Controllers")); { if(RoleClass) { TArray> NewControllerClasses = ULiveLinkControllerBase::GetControllersForRole(RoleClass); if (NewControllerClasses.Num() > 0) { //Always add a None entry MenuBuilder.AddMenuEntry( FText::FromName(NAME_None), FText::FromName(NAME_None), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FLiveLinkComponentDetailCustomization::HandleControllerSelection, RoleClass, TWeakObjectPtr()), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FLiveLinkComponentDetailCustomization::IsControllerItemSelected, FName(), RoleClass) ), NAME_None, EUserInterfaceActionType::RadioButton ); for (const TSubclassOf& ControllerClass : NewControllerClasses) { if (UClass* ControllerClassPtr = ControllerClass.Get()) { MenuBuilder.AddMenuEntry( FText::Format(LOCTEXT("Controller Label", "{0}"), ControllerClassPtr->GetDisplayNameText()), FText::Format(LOCTEXT("Controller ToolTip", "{0}"), ControllerClassPtr->GetDisplayNameText()), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FLiveLinkComponentDetailCustomization::HandleControllerSelection, RoleClass, MakeWeakObjectPtr(ControllerClassPtr)), FCanExecuteAction(), FIsActionChecked::CreateSP(this, &FLiveLinkComponentDetailCustomization::IsControllerItemSelected, ControllerClassPtr->GetFName(), RoleClass) ), NAME_None, EUserInterfaceActionType::RadioButton ); } } } else { MenuBuilder.AddWidget(SNullWidget::NullWidget, LOCTEXT("NoControllersFound", "No Controllers were found for this role"), false, false); } } else { MenuBuilder.AddWidget(SNullWidget::NullWidget, LOCTEXT("InvalidRoleClass", "Role is invalid. Can't find controllers for it"), false, false); } } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void FLiveLinkComponentDetailCustomization::HandleControllerSelection(TSubclassOf RoleClass, TWeakObjectPtr SelectedControllerClass) const { if (ULiveLinkComponentController* EditedObjectPtr = EditedObject.Get()) { const FScopedTransaction Transaction(LOCTEXT("OnChangedController", "Property Controller Selection")); EditedObjectPtr->Modify(); UClass* SelectedControllerClassPtr = SelectedControllerClass.Get(); EditedObjectPtr->SetControllerClassForRole(RoleClass, SelectedControllerClassPtr); } } bool FLiveLinkComponentDetailCustomization::IsControllerItemSelected(FName Item, TSubclassOf RoleClass) const { if (RoleClass == nullptr || RoleClass.Get() == nullptr) { return false; } if (ULiveLinkComponentController* EditedObjectPtr = EditedObject.Get()) { TObjectPtr* CurrentClass = EditedObjectPtr->ControllerMap.Find(RoleClass); if (CurrentClass == nullptr || *CurrentClass == nullptr) { return Item.IsNone(); } else { return (*CurrentClass)->GetClass()->GetFName() == Item; } } return false; } EVisibility FLiveLinkComponentDetailCustomization::HandleControllerWarningVisibility(TSubclassOf RoleClassEntry) const { if (ULiveLinkComponentController* EditorObjectPtr = EditedObject.Get()) { TObjectPtr* AssociatedControllerPtr = EditorObjectPtr->ControllerMap.Find(RoleClassEntry); if (AssociatedControllerPtr && *AssociatedControllerPtr) { const ULiveLinkControllerBase* AssociatedController = *AssociatedControllerPtr; if (UActorComponent* SelectedComponent = AssociatedController->GetAttachedComponent()) { if (!SelectedComponent->IsA(AssociatedController->GetDesiredComponentClass())) { //Controller exists for RoleClass and desired component is not the kind it wants return EVisibility::Visible; } } else { //Component is not valid return EVisibility::Visible; } } } return EVisibility::Hidden; } TSharedRef FLiveLinkComponentDetailCustomization::BuildControllerNameWidget(TSharedPtr ControllersProperty, TSubclassOf RoleClass) const { return ControllersProperty->CreatePropertyNameWidget(RoleClass.Get()->GetDisplayNameText()); } TSharedRef FLiveLinkComponentDetailCustomization::BuildControllerValueWidget(TSubclassOf RoleClass, const FText& ControllerName) const { TSharedRef ValueWidget = SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(FMargin(4.0f, 0.0f, 0.0f, 0.0f)) [ SNew(SComboButton) .OnGetMenuContent(this, &FLiveLinkComponentDetailCustomization::HandleControllerComboButton, RoleClass) .ContentPadding(FMargin(4.0, 2.0)) .ButtonContent() [ SNew(STextBlock) .Text(ControllerName) .Font(FAppStyle::Get().GetFontStyle("SmallFont")) ] ] + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) .Padding(FMargin(4.0f, 0.0f, 0.0f, 0.0f)) [ SNew(SImage) .Image(FLiveLinkEditorPrivate::GetStyleSet()->GetBrush("LiveLinkController.WarningIcon")) .ToolTipText(LOCTEXT("ControllerWarningToolTip", "The selected component to control does not match this controller's desired component class")) .Visibility(this, &FLiveLinkComponentDetailCustomization::HandleControllerWarningVisibility, RoleClass) ]; return ValueWidget; } void FLiveLinkComponentDetailCustomization::ForceRefreshDetails() { DetailLayout->ForceRefreshDetails(); } #undef LOCTEXT_NAMESPACE