// Copyright Epic Games, Inc. All Rights Reserved. #include "LiveLinkClientPanelViews.h" #include "Algo/Accumulate.h" #include "ClassViewerFilter.h" #include "ClassViewerModule.h" #include "Editor.h" #include "EditorFontGlyphs.h" #include "Features/IModularFeatures.h" #include "Framework/Commands/UICommandList.h" #include "IDetailsView.h" #include "ILiveLinkClient.h" #include "ILiveLinkModule.h" #include "Internationalization/Text.h" #include "LiveLinkClient.h" #include "LiveLinkClientCommands.h" #include "LiveLinkSettings.h" #include "LiveLinkSubjectSettings.h" #include "LiveLinkTypes.h" #include "LiveLinkVirtualSubject.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Modules/ModuleManager.h" #include "PropertyEditorModule.h" #include "SLiveLinkDataView.h" #include "SPositiveActionButton.h" #include "Styling/SlateStyle.h" #include "Styling/SlateStyleRegistry.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SSearchBox.h" #include "Widgets/Input/STextEntryPopup.h" #include "Widgets/Layout/SUniformGridPanel.h" #define LOCTEXT_NAMESPACE "LiveLinkClientPanel.PanelViews" // Static Source UI FNames namespace SourceListUI { static const FName TypeColumnName(TEXT("Type")); static const FName MachineColumnName(TEXT("Machine")); static const FName StatusColumnName(TEXT("Status")); static const FName ActionsColumnName(TEXT("Action")); }; // Static Subject UI FNames namespace SubjectTreeUI { static const FName EnabledColumnName(TEXT("Enabled")); static const FName NameColumnName(TEXT("Name")); static const FName RoleColumnName(TEXT("Role")); static const FName TranslatedRoleColumnName(TEXT("TranslatedRole")); static const FName ActionsColumnName(TEXT("Action")); }; namespace UE::LiveLink { TSharedPtr CreateSourcesDetailsView(const TSharedPtr& InSourcesView, const TAttribute& bInReadOnly) { FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bUpdatesFromSelection = false; DetailsViewArgs.bLockable = false; DetailsViewArgs.bShowPropertyMatrixButton = false; DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea; DetailsViewArgs.ViewIdentifier = NAME_None; TSharedPtr SettingsDetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); // todo: use controller here instead of view widget SettingsDetailsView->OnFinishedChangingProperties().AddRaw(InSourcesView.Get(), &FLiveLinkSourcesView::OnPropertyChanged); SettingsDetailsView->SetIsPropertyEditingEnabledDelegate(FIsPropertyEditingEnabled::CreateLambda( [bInReadOnly](){ return !bInReadOnly.Get(); })); return SettingsDetailsView; } TSharedPtr CreateSubjectsDetailsView(FLiveLinkClient* InLiveLinkClient, const TAttribute& bInReadOnly) { return SNew(SLiveLinkDataView, InLiveLinkClient) .ReadOnly(bInReadOnly); } } // namespace UE::LiveLink /** Dialog to create a new virtual subject */ class SVirtualSubjectCreateDialog : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SVirtualSubjectCreateDialog) {} /** Pointer to the LiveLinkClient instance. */ SLATE_ARGUMENT(FLiveLinkClient*, LiveLinkClient) SLATE_END_ARGS() /** Constructs this widget with InArgs */ void Construct(const FArguments& InArgs) { static const FName DefaultVirtualSubjectName = TEXT("Virtual"); bOkClicked = false; VirtualSubjectClass = nullptr; LiveLinkClient = InArgs._LiveLinkClient; check(LiveLinkClient); int32 NumVirtualSubjects = Algo::TransformAccumulate(LiveLinkClient->GetSubjects(true, true), [this](const FLiveLinkSubjectKey& SubjectKey) { return LiveLinkClient->IsVirtualSubject(SubjectKey) ? 1 : 0; }, 0); VirtualSubjectName = DefaultVirtualSubjectName; if (NumVirtualSubjects > 0) { VirtualSubjectName = *FString::Printf(TEXT("%s %d"), *DefaultVirtualSubjectName.ToString(), NumVirtualSubjects + 1); } //Default VirtualSubject Source should always exist TArray Sources = LiveLinkClient->GetVirtualSources(); check(Sources.Num() > 0); VirtualSourceGuid = Sources[0]; TSharedPtr TextEntry; SAssignNew(TextEntry, STextEntryPopup) .Label(LOCTEXT("AddVirtualSubjectName", "New Virtual Subject Name")) .DefaultText(FText::FromName(VirtualSubjectName)) .OnTextChanged(this, &SVirtualSubjectCreateDialog::HandleAddVirtualSubjectChanged); VirtualSubjectTextWidget = TextEntry; ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("Menu.Background")) [ SNew(SBox) [ SNew(SVerticalBox) +SVerticalBox::Slot() .HAlign(HAlign_Fill) .AutoHeight() [ TextEntry->AsShared() ] + SVerticalBox::Slot() .FillHeight(1.0) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Content() [ SAssignNew(RoleClassPicker, SVerticalBox) ] ] // Ok/Cancel buttons + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Right) .VAlign(VAlign_Bottom) .Padding(8) [ SNew(SUniformGridPanel) .SlotPadding(FAppStyle::GetMargin("StandardDialog.SlotPadding")) .MinDesiredSlotWidth(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotWidth")) .MinDesiredSlotHeight(FAppStyle::GetFloat("StandardDialog.MinDesiredSlotHeight")) + SUniformGridPanel::Slot(0, 0) [ SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked(this, &SVirtualSubjectCreateDialog::OkClicked) .Text(LOCTEXT("AddVirtualSubjectAdd", "Add")) .IsEnabled(this, &SVirtualSubjectCreateDialog::IsVirtualSubjectClassSelected) ] + SUniformGridPanel::Slot(1, 0) [ SNew(SButton) .HAlign(HAlign_Center) .ContentPadding(FAppStyle::GetMargin("StandardDialog.ContentPadding")) .OnClicked(this, &SVirtualSubjectCreateDialog::CancelClicked) .Text(LOCTEXT("AddVirtualSubjectCancel", "Cancel")) ] ] ] ] ]; MakeRoleClassPicker(); } bool IsVirtualSubjectClassSelected() const { return VirtualSubjectClass != nullptr; } bool ConfigureVirtualSubject() { TSharedRef Window = SNew(SWindow) .Title(LOCTEXT("CreateVirtualSubjectCreation", "Create Virtual Subject")) .ClientSize(FVector2D(400, 300)) .SupportsMinimize(false) .SupportsMaximize(false) [ AsShared() ]; PickerWindow = Window; GEditor->EditorAddModalWindow(Window); return bOkClicked; } private: /** Class filter to display virtual subjects classes for the role class picker.. */ class FLiveLinkRoleClassFilter : public IClassViewerFilter { public: FLiveLinkRoleClassFilter() = default; virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs) override { if (InClass->IsChildOf(ULiveLinkVirtualSubject::StaticClass())) { return InClass->GetDefaultObject()->GetRole() != nullptr && !InClass->HasAnyClassFlags(CLASS_Abstract | CLASS_HideDropDown | CLASS_Deprecated); } return false; } virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef< const IUnloadedBlueprintData > InUnloadedClassData, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs) override { return InUnloadedClassData->IsChildOf(ULiveLinkVirtualSubject::StaticClass()); } }; /** Creates the combo menu for the role class */ void MakeRoleClassPicker() { // Load the classviewer module to display a class picker FClassViewerModule& ClassViewerModule = FModuleManager::LoadModuleChecked("ClassViewer"); // Fill in options FClassViewerInitializationOptions Options; Options.Mode = EClassViewerMode::ClassPicker; Options.ClassFilters.Add(MakeShared()); RoleClassPicker->ClearChildren(); RoleClassPicker->AddSlot() .AutoHeight() [ SNew(STextBlock) .Text(LOCTEXT("VirtualSubjectRole", "Virtual Subject Role:")) .ShadowOffset(FVector2D(1.0f, 1.0f)) ]; RoleClassPicker->AddSlot() [ ClassViewerModule.CreateClassViewer(Options, FOnClassPicked::CreateSP(this, &SVirtualSubjectCreateDialog::OnClassPicked)) ]; } /** Handler for when a parent class is selected */ void OnClassPicked(UClass* ChosenClass) { VirtualSubjectClass = ChosenClass; } /** Handler for when ok is clicked */ FReply OkClicked() { if (LiveLinkClient) { const FLiveLinkSubjectKey NewVirtualSubjectKey(VirtualSourceGuid, VirtualSubjectName); LiveLinkClient->AddVirtualSubject(NewVirtualSubjectKey, VirtualSubjectClass); } CloseDialog(true); return FReply::Handled(); } /** Close the role picker window. */ void CloseDialog(bool bWasPicked = false) { bOkClicked = bWasPicked; if (PickerWindow.IsValid()) { PickerWindow.Pin()->RequestDestroyWindow(); } } /** Handler for when cancel is clicked */ FReply CancelClicked() { CloseDialog(); return FReply::Handled(); } /** Handles closing the role picker window when pressing escape. */ FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (InKeyEvent.GetKey() == EKeys::Escape) { CloseDialog(); return FReply::Handled(); } return SWidget::OnKeyDown(MyGeometry, InKeyEvent); } /** Handles setting the error when changing the name of a new virtual subject. */ void HandleAddVirtualSubjectChanged(const FText& NewSubjectName) { TSharedPtr VirtualSubjectTextWidgetPin = VirtualSubjectTextWidget.Pin(); if (VirtualSubjectTextWidgetPin.IsValid()) { TArray SubjectKey = LiveLinkClient->GetSubjects(true, true); FName SubjectName = *NewSubjectName.ToString(); const FLiveLinkSubjectKey ThisSubjectKey(VirtualSourceGuid, SubjectName); if (SubjectName.IsNone()) { VirtualSubjectTextWidgetPin->SetError(LOCTEXT("VirtualInvalidName", "Invalid Virtual Subject")); } else if (SubjectKey.FindByPredicate([ThisSubjectKey](const FLiveLinkSubjectKey& Key) { return Key == ThisSubjectKey; })) { VirtualSubjectTextWidgetPin->SetError(LOCTEXT("VirtualExistingName", "Subject already exist")); } else { VirtualSubjectName = SubjectName; VirtualSubjectTextWidgetPin->SetError(FText::GetEmpty()); } } } private: /** Cached livelink client. */ FLiveLinkClient* LiveLinkClient; /** Holds the text entry widget to name a virtual subject. */ TWeakPtr VirtualSubjectTextWidget; /** A pointer to the window that is asking the user to select a role class */ TWeakPtr PickerWindow; /** The container for the role Class picker */ TSharedPtr RoleClassPicker; /** The virtual subject's class */ TSubclassOf VirtualSubjectClass; /** Selected source guid */ FGuid VirtualSourceGuid; /** The virtual subject's name */ FName VirtualSubjectName; /** True if Ok was clicked */ bool bOkClicked; }; FGuid FLiveLinkSourceUIEntry::GetGuid() const { return EntryGuid; } FText FLiveLinkSourceUIEntry::GetSourceType() const { return Client->GetSourceType(EntryGuid); } FText FLiveLinkSourceUIEntry::GetMachineName() const { return Client->GetSourceMachineName(EntryGuid); } FText FLiveLinkSourceUIEntry::GetStatus() const { return Client->GetSourceStatus(EntryGuid); } ULiveLinkSourceSettings* FLiveLinkSourceUIEntry::GetSourceSettings() const { return Client->GetSourceSettings(EntryGuid); } void FLiveLinkSourceUIEntry::RemoveFromClient() const { Client->RemoveSource(EntryGuid); } FText FLiveLinkSourceUIEntry::GetDisplayName() const { return GetSourceType(); } FText FLiveLinkSourceUIEntry::GetToolTip() const { return Client->GetSourceToolTip(EntryGuid); } FLiveLinkSubjectUIEntry::FLiveLinkSubjectUIEntry(const FLiveLinkSubjectKey& InSubjectKey, FLiveLinkClient* InClient, bool bInIsSource) : SubjectKey(InSubjectKey) , Client(InClient) , bIsSource(bInIsSource) { if (InClient) { bIsVirtualSubject = InClient->IsVirtualSubject(InSubjectKey); } } bool FLiveLinkSubjectUIEntry::IsSubject() const { return !bIsSource; } bool FLiveLinkSubjectUIEntry::IsSource() const { return bIsSource; } bool FLiveLinkSubjectUIEntry::IsVirtualSubject() const { return IsSubject() && bIsVirtualSubject; } UObject* FLiveLinkSubjectUIEntry::GetSettings() const { if (IsSource()) { return Client->GetSourceSettings(SubjectKey.Source); } else { return Client->GetSubjectSettings(SubjectKey); } } bool FLiveLinkSubjectUIEntry::IsSubjectEnabled() const { return IsSubject() ? Client->IsSubjectEnabled(SubjectKey, false) : false; } bool FLiveLinkSubjectUIEntry::IsSubjectValid() const { return IsSubject() ? Client->IsSubjectValid(SubjectKey) : false; } void FLiveLinkSubjectUIEntry::SetSubjectEnabled(bool bIsEnabled) { if (IsSubject()) { Client->SetSubjectEnabled(SubjectKey, bIsEnabled); } } FText FLiveLinkSubjectUIEntry::GetItemText() const { if (IsSubject()) { return Client->GetSubjectDisplayName(SubjectKey); } else { return Client->GetSourceNameOverride(SubjectKey); } } TSubclassOf FLiveLinkSubjectUIEntry::GetItemRole() const { return IsSubject() ? Client->GetSubjectRole_AnyThread(SubjectKey) : TSubclassOf(); } TSubclassOf FLiveLinkSubjectUIEntry::GetItemTranslatedRole() const { return IsSubject() ? Client->GetSubjectTranslatedRole_AnyThread(SubjectKey) : TSubclassOf(); } void FLiveLinkSubjectUIEntry::RemoveFromClient() const { Client->RemoveSubject_AnyThread(SubjectKey); } void FLiveLinkSubjectUIEntry::PauseSubject() { if (IsPaused()) { Client->UnpauseSubject_AnyThread(SubjectKey.SubjectName); } else { Client->PauseSubject_AnyThread(SubjectKey.SubjectName); } } bool FLiveLinkSubjectUIEntry::IsPaused() const { return Client->GetSubjectState(SubjectKey.SubjectName) == ELiveLinkSubjectState::Paused; } class SLiveLinkClientPanelSubjectRow : public SMultiColumnTableRow { public: SLATE_BEGIN_ARGS(SLiveLinkClientPanelSubjectRow) {} /** The list item for this row */ SLATE_ARGUMENT(FLiveLinkSubjectUIEntryPtr, Entry) SLATE_ATTRIBUTE(bool, ReadOnly) SLATE_END_ARGS() void Construct(const FArguments& Args, const TSharedRef& OwnerTableView) { EntryPtr = Args._Entry; bReadOnly = Args._ReadOnly; TSharedPtr StyleSet = ILiveLinkModule::Get().GetStyle(); OkayIcon = StyleSet->GetBrush("LiveLink.Subject.Okay"); WarningIcon = StyleSet->GetBrush("LiveLink.Subject.Warning"); PauseIcon = StyleSet->GetBrush("LiveLink.Subject.Pause"); SMultiColumnTableRow::Construct( FSuperRowType::FArguments() .Style(&FAppStyle().GetWidgetStyle("TableView.AlternatingRow")) .Padding(1.0f), OwnerTableView ); } /** Overridden from SMultiColumnTableRow. Generates a widget for this column of the list view. */ virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override { if (ColumnName == SubjectTreeUI::EnabledColumnName) { if (EntryPtr->IsSubject()) { return SNew(SCheckBox) .Visibility(this, &SLiveLinkClientPanelSubjectRow::GetVisibilityFromReadOnly) .IsChecked(MakeAttributeSP(this, &SLiveLinkClientPanelSubjectRow::GetSubjectEnabled)) .OnCheckStateChanged(this, &SLiveLinkClientPanelSubjectRow::OnEnabledChanged); } } else if (ColumnName == SubjectTreeUI::NameColumnName) { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(6, 0, 0, 0) [ SNew(SExpanderArrow, SharedThis(this)).IndentAmount(12) ] + SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(STextBlock) .ColorAndOpacity(this, &SLiveLinkClientPanelSubjectRow::GetSubjectTextColor) .Text(MakeAttributeSP(this, &SLiveLinkClientPanelSubjectRow::GetItemText)) ]; } else if (ColumnName == SubjectTreeUI::RoleColumnName) { TAttribute RoleAttribute = MakeAttributeSP(this, &SLiveLinkClientPanelSubjectRow::GetItemRole); return SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .ColorAndOpacity(this, &SLiveLinkClientPanelSubjectRow::GetSubjectTextColor) .Text(EntryPtr->IsSubject() ? RoleAttribute : FText::GetEmpty()) ]; } else if (ColumnName == SubjectTreeUI::TranslatedRoleColumnName) { TAttribute TranslatedRoleAttribute = MakeAttributeSP(this, &SLiveLinkClientPanelSubjectRow::GetItemTranslatedRole); return SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ SNew(STextBlock) .ColorAndOpacity(this, &SLiveLinkClientPanelSubjectRow::GetSubjectTextColor) .Text(EntryPtr->IsSubject() ? TranslatedRoleAttribute : FText::GetEmpty()) ]; } else if (ColumnName == SubjectTreeUI::ActionsColumnName) { return SNew(SBox) .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SNew(SImage) .Image(this, &SLiveLinkClientPanelSubjectRow::GetSubjectIcon) .ToolTipText(this, &SLiveLinkClientPanelSubjectRow::GetSubjectIconToolTip) ]; } return SNullWidget::NullWidget; } private: ECheckBoxState GetSubjectEnabled() const { return EntryPtr->IsSubjectEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } void OnEnabledChanged(ECheckBoxState NewState) { EntryPtr->SetSubjectEnabled(NewState == ECheckBoxState::Checked); } FText GetItemText() const { return EntryPtr->GetItemText(); } FText GetItemRole() const { TSubclassOf Role = EntryPtr->GetItemRole(); if (Role) { return Role->GetDefaultObject()->GetDisplayName(); } return FText::GetEmpty(); } // If role is translated, put the original role in the extra info. FText GetItemTranslatedRole() const { TSubclassOf TranslatedRole = EntryPtr->GetItemTranslatedRole(); if (TranslatedRole.Get()) { return TranslatedRole->GetDefaultObject()->GetDisplayName(); } return FText::GetEmpty(); } /** Get widget visibility according to whether or not the panel is in read-only mode. */ EVisibility GetVisibilityFromReadOnly() const { return bReadOnly.Get() ? EVisibility::Collapsed : EVisibility::Visible; } /** Get the icon for a subject's status. */ const FSlateBrush* GetSubjectIcon() const { const FSlateBrush* IconBrush = nullptr; if (EntryPtr->IsSubjectEnabled()) { if (!EntryPtr->IsPaused()) { IconBrush = EntryPtr->IsSubjectValid() ? OkayIcon : WarningIcon; } else { IconBrush = PauseIcon; } } else { // No icon for disabled subjects, we rely on setting the text color to subdued foreground. } return IconBrush; } /** Get the tooltip for a subject's status icon. */ FText GetSubjectIconToolTip() const { FText IconToolTip; if (EntryPtr->IsSubjectEnabled()) { IconToolTip = EntryPtr->IsSubjectValid() ? LOCTEXT("ValidSubjectToolTip", "Subject is operating normally.") : LOCTEXT("InvalidSubjectToolTip", "Subject is invalid."); } else { IconToolTip = LOCTEXT("SubjectDisabledToolTip", "Subject is disabled."); } return IconToolTip; } /** Get the text color for a subject. */ FSlateColor GetSubjectTextColor() const { FSlateColor TextColor = FSlateColor::UseForeground(); if (EntryPtr->IsSubject() && !EntryPtr->IsSubjectEnabled()) { TextColor = FSlateColor::UseSubduedForeground(); } return TextColor; } FLiveLinkSubjectUIEntryPtr EntryPtr; /** Returns whether the panel is in read-only mode. */ TAttribute bReadOnly; //~ Status icons const FSlateBrush* OkayIcon = nullptr; const FSlateBrush* WarningIcon = nullptr; const FSlateBrush* PauseIcon = nullptr; }; class SLiveLinkClientPanelSourcesRow : public SMultiColumnTableRow { public: SLATE_BEGIN_ARGS(SLiveLinkClientPanelSourcesRow) {} /** The list item for this row */ SLATE_ARGUMENT(FLiveLinkSourceUIEntryPtr, Entry) SLATE_ATTRIBUTE(bool, ReadOnly) SLATE_END_ARGS() void Construct(const FArguments& Args, const TSharedRef& OwnerTableView) { EntryPtr = Args._Entry; bReadOnly = Args._ReadOnly; SMultiColumnTableRow::Construct( FSuperRowType::FArguments() .Style(&FAppStyle().GetWidgetStyle("TableView.AlternatingRow")) .Padding(1.0f), OwnerTableView ); } /** Overridden from SMultiColumnTableRow. Generates a widget for this column of the list view. */ virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override { if (ColumnName == SourceListUI::TypeColumnName) { return SNew(STextBlock) .Text(EntryPtr->GetSourceType()); } else if (ColumnName == SourceListUI::MachineColumnName) { return SNew(STextBlock) .Text(MakeAttributeSP(this, &SLiveLinkClientPanelSourcesRow::GetMachineName)); } else if (ColumnName == SourceListUI::StatusColumnName) { return SNew(STextBlock) .Text(MakeAttributeSP(this, &SLiveLinkClientPanelSourcesRow::GetSourceStatus)); } else if (ColumnName == SourceListUI::ActionsColumnName) { return SNew(SButton) .ButtonStyle(FAppStyle::Get(), "HoverHintOnly") .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Visibility(this, &SLiveLinkClientPanelSourcesRow::GetVisibilityFromReadOnly) .OnClicked(this, &SLiveLinkClientPanelSourcesRow::OnRemoveClicked) .ToolTipText(LOCTEXT("RemoveSource", "Remove selected live link source")) .ContentPadding(0.f) .ForegroundColor(FSlateColor::UseForeground()) .IsFocusable(false) [ SNew(SImage) .Image(FAppStyle::GetBrush("Icons.Delete")) .ColorAndOpacity(FSlateColor::UseForeground()) ]; } return SNullWidget::NullWidget; } FText GetToolTipText() const { return EntryPtr->GetToolTip(); } private: FText GetMachineName() const { return EntryPtr->GetMachineName(); } FText GetSourceStatus() const { return EntryPtr->GetStatus(); } FReply OnRemoveClicked() { EntryPtr->RemoveFromClient(); return FReply::Handled(); } /** Get widget visibility according to whether or not the panel is in read-only mode. */ EVisibility GetVisibilityFromReadOnly() const { return bReadOnly.Get() ? EVisibility::Collapsed : EVisibility::Visible; } private: FLiveLinkSourceUIEntryPtr EntryPtr; /** Attribute used to query whether the panel is in read only mode or not. */ TAttribute bReadOnly; }; FLiveLinkSourcesView::FLiveLinkSourcesView(FLiveLinkClient* InLiveLinkClient, TSharedPtr InCommandList, TAttribute bInReadOnly, FOnSourceSelectionChanged InOnSourceSelectionChanged) : Client(InLiveLinkClient) , OnSourceSelectionChangedDelegate(MoveTemp(InOnSourceSelectionChanged)) , bReadOnly(MoveTemp(bInReadOnly)) { TArray Results; GetDerivedClasses(ULiveLinkSourceFactory::StaticClass(), Results, true); for (UClass* SourceFactory : Results) { if (!SourceFactory->HasAllClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists) && CastChecked(SourceFactory->GetDefaultObject())->IsEnabled()) { Factories.Add(NewObject(GetTransientPackage(), SourceFactory)); } } Algo::StableSort(Factories, [](ULiveLinkSourceFactory* LHS, ULiveLinkSourceFactory* RHS) { return LHS->GetSourceDisplayName().CompareTo(RHS->GetSourceDisplayName()) <= 0; }); CreateSourcesListView(InCommandList); } FLiveLinkSourcesView::~FLiveLinkSourcesView() { FTSTicker::RemoveTicker(TickHandle); } TSharedRef FLiveLinkSourcesView::MakeSourceListViewWidget(FLiveLinkSourceUIEntryPtr Entry, const TSharedRef& OwnerTable) const { return SNew(SLiveLinkClientPanelSourcesRow, OwnerTable) .Entry(Entry) .ReadOnly(bReadOnly) .ToolTipText_Lambda([Entry]() { return Entry->GetToolTip(); }); } void FLiveLinkSourcesView::OnSourceListSelectionChanged(FLiveLinkSourceUIEntryPtr Entry, ESelectInfo::Type SelectionType) const { OnSourceSelectionChangedDelegate.Execute(Entry, SelectionType); } void FLiveLinkSourcesView::CreateSourcesListView(const TSharedPtr& InCommandList) { SAssignNew(HostWidget, SVerticalBox) +SVerticalBox::Slot() .Padding(FMargin(4.0, 4.0, 4.0, 6.0)) .MinHeight(29) .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(4.0, 2.0)) .FillWidth(1.0f) [ SAssignNew(FilterSearchBox, SLiveLinkFilterSearchBox) .ItemSource(&SourceData) .OnGatherItems_Lambda([this](TArray& Items) { Items.Append(SourceData); }) .OnUpdateFilteredList_Lambda([this](const TArray& FilteredItems) { FilteredList = FilteredItems; SourcesListView->RequestListRefresh(); }) ] + SHorizontalBox::Slot() .Padding(FMargin(4.0, 2.0)) .AutoWidth() [ SNew(SPositiveActionButton) .OnGetMenuContent_Raw(this, &FLiveLinkSourcesView::OnGenerateSourceMenu) .Icon(FAppStyle::Get().GetBrush("Icons.Plus")) .Text(LOCTEXT("AddSource", "Add Source")) .ToolTipText(LOCTEXT("AddSource_ToolTip", "Add a new Live Link source")) ] ] +SVerticalBox::Slot() .VAlign(VAlign_Fill) [ SAssignNew(SourcesListView, SLiveLinkSourceListView, bReadOnly) .ListItemsSource(&FilteredList) .SelectionMode(ESelectionMode::Single) .ScrollbarVisibility(EVisibility::Visible) .OnGenerateRow_Raw(this, &FLiveLinkSourcesView::MakeSourceListViewWidget) .OnContextMenuOpening_Raw(this, &FLiveLinkSourcesView::OnSourceConstructContextMenu, InCommandList) .OnSelectionChanged_Raw(this, &FLiveLinkSourcesView::OnSourceListSelectionChanged) .HeaderRow ( SNew(SHeaderRow) + SHeaderRow::Column(SourceListUI::TypeColumnName) .FillWidth(25.f) .DefaultLabel(LOCTEXT("TypeColumnHeaderName", "Source Type")) + SHeaderRow::Column(SourceListUI::MachineColumnName) .FillWidth(25.f) .DefaultLabel(LOCTEXT("MachineColumnHeaderName", "Source Machine")) + SHeaderRow::Column(SourceListUI::StatusColumnName) .FillWidth(50.f) .DefaultLabel(LOCTEXT("StatusColumnHeaderName", "Status")) + SHeaderRow::Column(SourceListUI::ActionsColumnName) .ManualWidth(20.f) .DefaultLabel(FText()) ) ]; } TSharedPtr FLiveLinkSourcesView::OnSourceConstructContextMenu(TSharedPtr InCommandList) { if (bReadOnly.Get()) { return nullptr; } const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, InCommandList); MenuBuilder.BeginSection(TEXT("Remove")); { if (CanRemoveSource()) { MenuBuilder.AddMenuEntry(FLiveLinkClientCommands::Get().RemoveSource); } MenuBuilder.AddMenuEntry(FLiveLinkClientCommands::Get().RemoveAllSources); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } TSharedRef FLiveLinkSourcesView::GetWidget() { return HostWidget.ToSharedRef(); } void FLiveLinkSourcesView::RefreshSourceData(bool bRefreshUI) { SourceData.Reset(); FilteredList.Reset(); for (FGuid SourceGuid : Client->GetDisplayableSources()) { SourceData.Add(MakeShared(SourceGuid, Client)); } SourceData.Sort([](const FLiveLinkSourceUIEntryPtr& LHS, const FLiveLinkSourceUIEntryPtr& RHS) { return LHS->GetMachineName().CompareTo(RHS->GetMachineName()) < 0; }); if (bRefreshUI && FilterSearchBox) { FilterSearchBox->Update(); } } void FLiveLinkSourcesView::HandleRemoveSource() { TArray Selected; SourcesListView->GetSelectedItems(Selected); if (Selected.Num() > 0) { Selected[0]->RemoveFromClient(); } } bool FLiveLinkSourcesView::CanRemoveSource() { return SourcesListView->GetNumItemsSelected() > 0; } FLiveLinkSubjectsView::FLiveLinkSubjectsView(FOnSubjectSelectionChanged InOnSubjectSelectionChanged, const TSharedPtr& InCommandList, TAttribute bInReadOnly) : SubjectSelectionChangedDelegate(InOnSubjectSelectionChanged) , bReadOnly(MoveTemp(bInReadOnly)) { CreateSubjectsTreeView(InCommandList); } void FLiveLinkSubjectsView::OnSubjectSelectionChanged(FLiveLinkSubjectUIEntryPtr SubjectEntry, ESelectInfo::Type SelectInfo) { SubjectSelectionChangedDelegate.Execute(SubjectEntry, SelectInfo); } void FLiveLinkSourcesView::OnPropertyChanged(const FPropertyChangedEvent& InEvent) { TArray Selected; SourcesListView->GetSelectedItems(Selected); for (FLiveLinkSourceUIEntryPtr Item : Selected) { Client->OnPropertyChanged(Item->GetGuid(), InEvent); } } TSharedRef FLiveLinkSubjectsView::MakeTreeRowWidget(FLiveLinkSubjectUIEntryPtr InInfo, const TSharedRef& OwnerTable) { return SNew(SLiveLinkClientPanelSubjectRow, OwnerTable) .Entry(InInfo) .ReadOnly(bReadOnly); } void FLiveLinkSubjectsView::GetChildrenForInfo(FLiveLinkSubjectUIEntryPtr InInfo, TArray< FLiveLinkSubjectUIEntryPtr >& OutChildren) { OutChildren = InInfo->Children; } TSharedPtr FLiveLinkSubjectsView::OnOpenVirtualSubjectContextMenu(TSharedPtr InCommandList) { if (bReadOnly.Get()) { return nullptr; } constexpr bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, InCommandList); MenuBuilder.BeginSection(TEXT("Remove")); { if (CanRemoveSubject()) { MenuBuilder.AddMenuEntry(FLiveLinkClientCommands::Get().RemoveSubject); } } MenuBuilder.EndSection(); MenuBuilder.AddMenuEntry(FLiveLinkClientCommands::Get().PauseSubject, NAME_None, TAttribute::CreateSP(this, &FLiveLinkSubjectsView::GetPauseSubjectLabel), TAttribute::CreateSP(this, &FLiveLinkSubjectsView::GetPauseSubjectToolTip)); return MenuBuilder.MakeWidget(); } bool FLiveLinkSubjectsView::CanRemoveSubject() const { TArray Selected; SubjectsTreeView->GetSelectedItems(Selected); for (const FLiveLinkSubjectUIEntryPtr& Entry : Selected) { if (Entry && Entry->IsVirtualSubject()) { return true; } } return false; } void FLiveLinkSubjectsView::RefreshSubjects() { TArray> SavedSelection; { TArray SelectedItems = SubjectsTreeView->GetSelectedItems(); for (const FLiveLinkSubjectUIEntryPtr& SelectedItem : SelectedItems) { SavedSelection.Add(TPair(SelectedItem->SubjectKey, SelectedItem->IsSubject())); } } if (IModularFeatures::Get().IsModularFeatureAvailable(ILiveLinkClient::ModularFeatureName)) { if (ILiveLinkClient* Client = &IModularFeatures::Get().GetModularFeature(ILiveLinkClient::ModularFeatureName)) { TArray SubjectKeys = Client->GetSubjects(true, true); SubjectData.Reset(); TMap SourceItems; TArray AllItems; AllItems.Reserve(SubjectKeys.Num()); for (const FLiveLinkSubjectKey& SubjectKey : SubjectKeys) { FLiveLinkSubjectUIEntryPtr Source; FName SourceNameOverride = *Client->GetSourceNameOverride(SubjectKey).ToString(); if (FLiveLinkSubjectUIEntryPtr* SourcePtr = SourceItems.Find(*SourceNameOverride.ToString())) { Source = *SourcePtr; } else { constexpr bool bIsSource = true; Source = MakeShared(SubjectKey, static_cast(Client), bIsSource); SubjectData.Add(Source); SourceItems.Add(SourceNameOverride) = Source; SubjectsTreeView->SetItemExpansion(Source, true); AllItems.Add(Source); } FLiveLinkSubjectUIEntryPtr SubjectEntry = MakeShared(SubjectKey, static_cast(Client)); Source->Children.Add(SubjectEntry); AllItems.Add(SubjectEntry); } auto SortPredicate = [](const FLiveLinkSubjectUIEntryPtr& LHS, const FLiveLinkSubjectUIEntryPtr& RHS) {return LHS->GetItemText().CompareTo(RHS->GetItemText()) < 0; }; SubjectData.Sort(SortPredicate); for (FLiveLinkSubjectUIEntryPtr& Subject : SubjectData) { Subject->Children.Sort(SortPredicate); } for (const FLiveLinkSubjectUIEntryPtr& Item : AllItems) { for (const TPair& Selection : SavedSelection) { if (Item->SubjectKey == Selection.Key && Item->IsSubject() == Selection.Value) { SubjectsTreeView->SetItemSelection(Item, true); break; } } } if (FilterSearchBox) { FilterSearchBox->Update(); } } } } bool FLiveLinkSubjectsView::CanPauseSubject() const { TArray Selected; SubjectsTreeView->GetSelectedItems(Selected); for (const FLiveLinkSubjectUIEntryPtr& Entry : Selected) { if (Entry && Entry->IsSubjectValid() && Entry->IsSubjectEnabled()) { return true; } } return false; } void FLiveLinkSubjectsView::HandlePauseSubject() { TArray Selected; SubjectsTreeView->GetSelectedItems(Selected); for (const FLiveLinkSubjectUIEntryPtr& Entry : Selected) { if (Entry && Entry->IsSubjectValid() && Entry->IsSubjectEnabled()) { Entry->PauseSubject(); } } } TSharedRef FLiveLinkSubjectsView::GetWidget() { return SNew(SVerticalBox) + SVerticalBox::Slot() .Padding(FMargin(4.0, 8.0, 4.0, 6.0)) .MinHeight(29) .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(FMargin(4.0, 2.0)) .FillWidth(1.0f) [ SAssignNew(FilterSearchBox, SLiveLinkFilterSearchBox) .ItemSource(&SubjectData) .OnGatherItems_Lambda([this](TArray& Items) { for (FLiveLinkSubjectUIEntryPtr Source : SubjectData) { for (FLiveLinkSubjectUIEntryPtr Subject : Source->Children) { Items.Add(Subject); } } }) .OnUpdateFilteredList_Lambda([this](const TArray& FilteredItems) { FilteredList = FilteredItems; for (const FLiveLinkSubjectUIEntryPtr& Item : FilteredList) { SubjectsTreeView->SetItemExpansion(Item, true); } SubjectsTreeView->RequestListRefresh(); }) ] ] + SVerticalBox::Slot() .VAlign(VAlign_Fill) [ SubjectsTreeView.ToSharedRef() ]; } void FLiveLinkSubjectsView::CreateSubjectsTreeView(const TSharedPtr& InCommandList) { SAssignNew(SubjectsTreeView, SLiveLinkSubjectsTreeView, bReadOnly) .TreeItemsSource(&FilteredList) .OnGenerateRow_Raw(this, &FLiveLinkSubjectsView::MakeTreeRowWidget) .OnGetChildren_Raw(this, &FLiveLinkSubjectsView::GetChildrenForInfo) .OnSelectionChanged_Raw(this, &FLiveLinkSubjectsView::OnSubjectSelectionChanged) .OnContextMenuOpening_Raw(this, &FLiveLinkSubjectsView::OnOpenVirtualSubjectContextMenu, InCommandList) .SelectionMode(ESelectionMode::Multi) .HeaderRow ( SNew(SHeaderRow) .CanSelectGeneratedColumn(true) .HiddenColumnsList({ SubjectTreeUI::TranslatedRoleColumnName }) + SHeaderRow::Column(SubjectTreeUI::EnabledColumnName) .DefaultTooltip(LOCTEXT("EnabledToolTip", "Whether this subject should be evaluated.")) .DefaultLabel(FText()) .FixedWidth(22) + SHeaderRow::Column(SubjectTreeUI::NameColumnName) .DefaultLabel(LOCTEXT("SubjectItemName", "Subject Name")) .DefaultTooltip(LOCTEXT("NameToolTip", "Unique name for this subject.")) .FillWidth(0.60f) + SHeaderRow::Column(SubjectTreeUI::RoleColumnName) .DefaultLabel(LOCTEXT("RoleName", "Role")) .ManualWidth(90.f) .DefaultTooltip(LOCTEXT("RoleToolTip", "Role of this subject.")) + SHeaderRow::Column(SubjectTreeUI::TranslatedRoleColumnName) .DefaultLabel(LOCTEXT("TranslatedRoleName", "Translated")) .DefaultTooltip(LOCTEXT("TranslatedRoleToolTip", "Get the translated role of a subject if it's being translated before being rebroadcast. This should only be relevant in Live Link Hub.")) .ManualWidth(70.f) + SHeaderRow::Column(SubjectTreeUI::ActionsColumnName) .ManualWidth(20.f) .DefaultLabel(FText()) ); } FText FLiveLinkSubjectsView::GetPauseSubjectLabel() const { FText Label = LOCTEXT("PauseSubjectLabel", "Pause Subject"); if (IsSelectedSubjectPaused()) { Label = LOCTEXT("UnpauseSubjectLabel", "Unpause Subject"); } return Label; } FText FLiveLinkSubjectsView::GetPauseSubjectToolTip() const { FText ToolTip = LOCTEXT("PauseSubjectToolTip", "Pause a subject, the last received data will be used for evaluation."); if (IsSelectedSubjectPaused()) { ToolTip = LOCTEXT("UnpauseSubjectToolTip", "Unpause Subject and resume operating on live data."); } return ToolTip; } bool FLiveLinkSubjectsView::IsSelectedSubjectPaused() const { TArray Selected; SubjectsTreeView->GetSelectedItems(Selected); for (const FLiveLinkSubjectUIEntryPtr& EntryPtr : Selected) { if (EntryPtr && !EntryPtr->IsPaused()) { return false; } } return true; } TSharedRef FLiveLinkSourcesView::OnGenerateSourceMenu() { const bool CloseAfterSelection = true; FMenuBuilder MenuBuilder(CloseAfterSelection, NULL); MenuBuilder.BeginSection("SourceSection", LOCTEXT("Sources", "Live Link Sources")); for (int32 FactoryIndex = 0; FactoryIndex < Factories.Num(); ++FactoryIndex) { ULiveLinkSourceFactory* FactoryInstance = Factories[FactoryIndex]; if (FactoryInstance) { ULiveLinkSourceFactory::EMenuType MenuType = FactoryInstance->GetMenuType(); if (MenuType == ULiveLinkSourceFactory::EMenuType::SubPanel) { MenuBuilder.AddMenuEntry( FactoryInstance->GetSourceDisplayName(), FactoryInstance->GetSourceTooltip(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FLiveLinkSourcesView::OpenCreateMenuWindow, FactoryIndex) ), NAME_None, EUserInterfaceActionType::Button); } else if (MenuType == ULiveLinkSourceFactory::EMenuType::MenuEntry) { MenuBuilder.AddMenuEntry( FactoryInstance->GetSourceDisplayName(), FactoryInstance->GetSourceTooltip(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FLiveLinkSourcesView::ExecuteCreateSource, FactoryIndex) ), NAME_None, EUserInterfaceActionType::Button); } else { MenuBuilder.AddMenuEntry( FactoryInstance->GetSourceDisplayName(), FactoryInstance->GetSourceTooltip(), FSlateIcon(), FUIAction( FExecuteAction(), FCanExecuteAction::CreateLambda([]() { return false; }) ), NAME_None, EUserInterfaceActionType::Button); } } } MenuBuilder.EndSection(); MenuBuilder.BeginSection("VirtualSourceSection", LOCTEXT("VirtualSources", "Live Link VirtualSubject Sources")); MenuBuilder.AddMenuEntry( LOCTEXT("AddVirtualSubject", "Add Virtual Subject"), LOCTEXT("AddVirtualSubject_Tooltip", "Adds a new virtual subject to Live Link. Instead of coming from a source a virtual subject is a combination of 2 or more real subjects"), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(this, &FLiveLinkSourcesView::AddVirtualSubject) ), NAME_None, EUserInterfaceActionType::Button); MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void FLiveLinkSourcesView::OpenCreateMenuWindow(int32 FactoryIndex) { if (Factories.IsValidIndex(FactoryIndex)) { if (ULiveLinkSourceFactory* FactoryInstance = Factories[FactoryIndex]) { TSharedPtr Widget = FactoryInstance->BuildCreationPanel(ULiveLinkSourceFactory::FOnLiveLinkSourceCreated::CreateSP(this, &FLiveLinkSourcesView::OnSourceCreated, TSubclassOf(FactoryInstance->GetClass()))); if (Widget.IsValid()) { FText CreateText = FText::Format(LOCTEXT("CreateSourceLabel", "Create {0} connection"), FactoryInstance->GetSourceDisplayName()); SourceCreationWindow = SNew(SWindow) .Title(CreateText) .ClientSize(FVector2D(400, 150)) .SizingRule(ESizingRule::Autosized) .SupportsMinimize(false) .SupportsMaximize(false) [ Widget.ToSharedRef() ]; GEditor->EditorAddModalWindow(SourceCreationWindow.ToSharedRef()); } } } } void FLiveLinkSourcesView::ExecuteCreateSource(int32 FactoryIndex) { if (Factories.IsValidIndex(FactoryIndex)) { if (ULiveLinkSourceFactory* FactoryInstance = Factories[FactoryIndex]) { OnSourceCreated(FactoryInstance->CreateSource(FString()), FString(), FactoryInstance->GetClass()); } } } void FLiveLinkSourcesView::OnSourceCreated(TSharedPtr NewSource, FString ConnectionString, TSubclassOf Factory) { if (NewSource.IsValid()) { FGuid NewSourceGuid = Client->AddSource(NewSource); if (NewSourceGuid.IsValid()) { if (ULiveLinkSourceSettings* Settings = Client->GetSourceSettings(NewSourceGuid)) { Settings->ConnectionString = ConnectionString; Settings->Factory = Factory; } } } if (SourceCreationWindow) { SourceCreationWindow->RequestDestroyWindow(); SourceCreationWindow.Reset(); } } void FLiveLinkSourcesView::AddVirtualSubject() { TSharedRef Dialog = SNew(SVirtualSubjectCreateDialog) .LiveLinkClient(Client); Dialog->ConfigureVirtualSubject(); } #undef LOCTEXT_NAMESPACE /**LiveLinkClientPanel*/