// Copyright Epic Games, Inc. All Rights Reserved. #include "WidgetBlueprintEditorUtils.h" #include "Components/PanelSlot.h" #include "Components/PanelWidget.h" #include "Components/ContentWidget.h" #include "Engine/Texture2D.h" #include "Interfaces/IPluginManager.h" #include "UObject/GCObjectScopeGuard.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "Internationalization/TextPackageNamespaceUtil.h" #include "UObject/PropertyPortFlags.h" #include "UObject/TopLevelAssetPath.h" #include "Blueprint/WidgetTree.h" #include "Misc/ConfigCacheIni.h" #include "Misc/PathViews.h" #include "Modules/ModuleManager.h" #include "MovieScene.h" #include "UMGEditorProjectSettings.h" #include "WidgetBlueprint.h" #include "HAL/PlatformApplicationMisc.h" #include "Settings/ContentBrowserSettings.h" #include "ClassViewerFilter.h" #include "EditorClassUtils.h" #include "Dialogs/Dialogs.h" #include "DragAndDrop/DecoratedDragDropOp.h" #include "DragAndDrop/AssetDragDropOp.h" #include "DragAndDrop/ClassDragDropOp.h" #include "DragDrop/WidgetTemplateDragDropOp.h" #include "Exporters/Exporter.h" #include "ObjectEditorUtils.h" #include "Components/CanvasPanelSlot.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Animation/WidgetAnimation.h" #include "Kismet2/Kismet2NameValidators.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Templates/WidgetTemplateClass.h" #include "Templates/WidgetTemplateImageClass.h" #include "Templates/WidgetTemplateBlueprintClass.h" #include "Factories.h" #include "UnrealExporter.h" #include "Framework/Commands/GenericCommands.h" #include "ScopedTransaction.h" #include "Components/CanvasPanel.h" #include "Utility/WidgetSlotPair.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Components/Widget.h" #include "Blueprint/WidgetNavigation.h" #include "Subsystems/AssetEditorSubsystem.h" #include "UObject/CoreRedirects.h" #include "UObject/ScriptInterface.h" #include "Components/NamedSlotInterface.h" #include "K2Node_Variable.h" #include "Engine/TextureRenderTarget2D.h" #include "Engine/UserInterfaceSettings.h" #include "Input/HittestGrid.h" #include "Interfaces/ISlateRHIRendererModule.h" #include "Interfaces/ISlate3DRenderer.h" #include "Rendering/SlateDrawBuffer.h" #include "Slate/WidgetRenderer.h" #include "UMGEditorModule.h" #include "Widgets/SVirtualWindow.h" #include "GraphEditorActions.h" #include "WidgetEditingProjectSettings.h" #include "UIComponentUtils.h" #define LOCTEXT_NAMESPACE "UMG" class FWidgetObjectTextFactory : public FCustomizableTextObjectFactory { public: FWidgetObjectTextFactory() : FCustomizableTextObjectFactory(GWarn) { } // FCustomizableTextObjectFactory implementation virtual bool CanCreateClass(UClass* ObjectClass, bool& bOmitSubObjs) const override { const bool bIsWidget = ObjectClass->IsChildOf(UWidget::StaticClass()); const bool bIsSlot = ObjectClass->IsChildOf(UPanelSlot::StaticClass()); const bool bIsSlotMetaData = ObjectClass->IsChildOf(UWidgetSlotPair::StaticClass()); return bIsWidget || bIsSlot || bIsSlotMetaData; } virtual void ProcessConstructedObject(UObject* NewObject) override { check(NewObject); if ( UWidget* Widget = Cast(NewObject) ) { NewWidgetMap.Add(Widget->GetFName(), Widget); } else if ( UWidgetSlotPair* SlotMetaData = Cast(NewObject) ) { MissingSlotData.Add(SlotMetaData->GetWidgetName(), SlotMetaData); } } // FCustomizableTextObjectFactory (end) public: // Name->Instance object mapping TMap NewWidgetMap; // Instance->OldSlotMetaData that didn't survive the journey because it wasn't copied. TMap MissingSlotData; }; FName SanitizeWidgetName(const FString& NewName, const FName CurrentName) { FString GeneratedName = SlugStringForValidName(NewName); // If the new name is empty (for example, because it was composed entirely of invalid characters). // then we'll use the current name if (GeneratedName.IsEmpty()) { return CurrentName; } const FName GeneratedFName(*GeneratedName); check(GeneratedFName.IsValidXName(INVALID_OBJECTNAME_CHARACTERS)); return GeneratedFName; } bool FWidgetBlueprintEditorUtils::VerifyWidgetRename(TSharedRef BlueprintEditor, FWidgetReference Widget, const FText& NewName, FText& OutErrorMessage) { if (NewName.IsEmptyOrWhitespace()) { OutErrorMessage = LOCTEXT("EmptyWidgetName", "Empty Widget Name"); return false; } const FString& NewNameString = NewName.ToString(); if (NewNameString.Len() >= NAME_SIZE) { OutErrorMessage = LOCTEXT("WidgetNameTooLong", "Widget Name is Too Long"); return false; } UWidget* RenamedTemplateWidget = Widget.GetTemplate(); if ( !RenamedTemplateWidget ) { // In certain situations, the template might be lost due to mid recompile with focus lost on the rename box at // during a strange moment. return false; } // Slug the new name down to a valid object name const FName NewNameSlug = SanitizeWidgetName(NewNameString, RenamedTemplateWidget->GetFName()); UWidgetBlueprint* Blueprint = BlueprintEditor->GetWidgetBlueprintObj(); UWidget* ExistingTemplate = Blueprint->WidgetTree->FindWidget(NewNameSlug); bool bIsSameWidget = false; if (ExistingTemplate != nullptr) { if ( RenamedTemplateWidget != ExistingTemplate ) { OutErrorMessage = LOCTEXT("ExistingWidgetName", "Existing Widget Name"); return false; } else { bIsSameWidget = true; } } else { // Not an existing widget in the tree BUT it still mustn't create a UObject name clash UWidget* WidgetPreview = Widget.GetPreview(); if (WidgetPreview) { // Dummy rename with flag REN_Test returns if rename is possible if (!WidgetPreview->Rename(*NewNameSlug.ToString(), nullptr, REN_Test)) { OutErrorMessage = LOCTEXT("ExistingObjectName", "Existing Object Name"); return false; } } UWidget* WidgetTemplate = RenamedTemplateWidget; // Dummy rename with flag REN_Test returns if rename is possible if (!WidgetTemplate->Rename(*NewNameSlug.ToString(), nullptr, REN_Test)) { OutErrorMessage = LOCTEXT("ExistingObjectName", "Existing Object Name"); return false; } } FObjectPropertyBase* Property = CastField(Blueprint->ParentClass->FindPropertyByName( NewNameSlug )); if ( Property && FWidgetBlueprintEditorUtils::IsBindWidgetProperty(Property)) { if (!RenamedTemplateWidget->IsA(Property->PropertyClass)) { OutErrorMessage = FText::Format(LOCTEXT("WidgetBindingOfWrongType", "Widget Binding is not type {0}"), Property->PropertyClass->GetDisplayNameText()); return false; } return true; } FKismetNameValidator Validator(Blueprint); // For variable comparison, use the slug EValidatorResult ValidatorResult = Validator.IsValid(NewNameSlug); if (ValidatorResult != EValidatorResult::Ok) { if (bIsSameWidget && (ValidatorResult == EValidatorResult::AlreadyInUse || ValidatorResult == EValidatorResult::ExistingName)) { // Continue successfully } else { OutErrorMessage = INameValidatorInterface::GetErrorText(NewNameString, ValidatorResult); return false; } } return true; } void FWidgetBlueprintEditorUtils::SetDesiredFocus(TSharedRef BlueprintEditor, const FName DesiredFocusWidgetName) { SetDesiredFocus(BlueprintEditor->GetWidgetBlueprintObj(), DesiredFocusWidgetName); } void FWidgetBlueprintEditorUtils::SetDesiredFocus(UWidgetBlueprint* Blueprint, const FName& DesiredFocusWidgetName) { if (Blueprint) { if (Blueprint->GeneratedClass) { if (UUserWidget* WidgetCDO = Blueprint->GeneratedClass->GetDefaultObject()) { WidgetCDO->SetFlags(RF_Transactional); WidgetCDO->Modify(); WidgetCDO->SetDesiredFocusWidget(DesiredFocusWidgetName); } } constexpr bool bFocusIfOpen = false; FWidgetBlueprintEditor* BlueprintEditor = static_cast(GEditor->GetEditorSubsystem()->FindEditorForAsset(Blueprint, bFocusIfOpen)); if (BlueprintEditor) { if (UUserWidget* PreviewWidget = BlueprintEditor->GetPreview()) { // We need to change the PreviewWidget to make sure the DetailPanel show the right value. PreviewWidget->SetFlags(RF_Transactional); PreviewWidget->Modify(); PreviewWidget->SetDesiredFocusWidget(DesiredFocusWidgetName); } } } } void FWidgetBlueprintEditorUtils::ReplaceDesiredFocus(TSharedRef BlueprintEditor, const FName& OldName, const FName& NewName) { ReplaceDesiredFocus(BlueprintEditor->GetWidgetBlueprintObj(), OldName, NewName); } void FWidgetBlueprintEditorUtils::ReplaceDesiredFocus(UWidgetBlueprint* Blueprint, const FName& OldName, const FName& NewName) { if (Blueprint && Blueprint->GeneratedClass) { if (UUserWidget* WidgetCDO = Blueprint->GeneratedClass->GetDefaultObject()) { // Verify if the Name changed is the Desired focus Widget name. if (WidgetCDO->GetDesiredFocusWidgetName() == OldName) { WidgetCDO->SetFlags(RF_Transactional); WidgetCDO->Modify(); WidgetCDO->SetDesiredFocusWidget(NewName); constexpr bool bFocusIfOpen = false; FWidgetBlueprintEditor* BlueprintEditor = static_cast(GEditor->GetEditorSubsystem()->FindEditorForAsset(Blueprint, bFocusIfOpen)); if (BlueprintEditor) { if (UUserWidget* PreviewWidget = BlueprintEditor->GetPreview()) { ensure(PreviewWidget->GetDesiredFocusWidgetName() == OldName); // We need to change the PreviewWidget to make sure the DetailPanel show the right value. PreviewWidget->SetFlags(RF_Transactional); PreviewWidget->Modify(); PreviewWidget->SetDesiredFocusWidget(NewName); } } } } } } bool FWidgetBlueprintEditorUtils::RenameWidget(TSharedRef BlueprintEditor, const FName& OldObjectName, const FString& NewDisplayName) { UWidgetBlueprint* Blueprint = BlueprintEditor->GetWidgetBlueprintObj(); check(Blueprint); UWidget* Widget = Blueprint->WidgetTree->FindWidget(OldObjectName); check(Widget); UClass* ParentClass = Blueprint->ParentClass; check( ParentClass ); bool bRenamed = false; TSharedPtr NameValidator = MakeShareable(new FKismetNameValidator(Blueprint, OldObjectName)); const FName NewFName = SanitizeWidgetName(NewDisplayName, Widget->GetFName()); FObjectPropertyBase* ExistingProperty = CastField(ParentClass->FindPropertyByName(NewFName)); const bool bBindWidget = ExistingProperty && FWidgetBlueprintEditorUtils::IsBindWidgetProperty(ExistingProperty) && Widget->IsA(ExistingProperty->PropertyClass); // NewName should be already validated. But one must make sure that NewTemplateName is also unique. const bool bUniqueNameForTemplate = ( EValidatorResult::Ok == NameValidator->IsValid( NewFName ) || bBindWidget ); if ( bUniqueNameForTemplate ) { // Stringify the FNames const FString NewNameStr = NewFName.ToString(); const FString OldNameStr = OldObjectName.ToString(); const FScopedTransaction Transaction(LOCTEXT("RenameWidget", "Rename Widget")); // Rename Template Blueprint->Modify(); Widget->Modify(); Blueprint->OnVariableRenamed(OldObjectName, NewFName); // Rename Preview before renaming the template widget so the preview widget can be found UWidget* WidgetPreview = BlueprintEditor->GetReferenceFromTemplate(Widget).GetPreview(); if (WidgetPreview) { WidgetPreview->SetDisplayLabel(NewDisplayName); WidgetPreview->Rename(*NewNameStr); } if (!WidgetPreview || WidgetPreview != Widget) { // Find and update all variable references in the graph Widget->SetDisplayLabel(NewDisplayName); Widget->Rename(*NewNameStr); } #if UE_HAS_WIDGET_GENERATED_BY_CLASS // When a widget gets renamed we need to check any existing blueprint getters that may be placed // in the graphs to fix up their state if(Widget->bIsVariable) { TArray AllGraphs; Blueprint->GetAllGraphs(AllGraphs); for (const UEdGraph* CurrentGraph : AllGraphs) { TArray GraphNodes; CurrentGraph->GetNodesOfClass(GraphNodes); for (UK2Node_Variable* CurrentNode : GraphNodes) { UClass* SelfClass = Blueprint->GeneratedClass; UClass* VariableParent = CurrentNode->VariableReference.GetMemberParentClass(SelfClass); if (SelfClass == VariableParent) { // Reconstruct this node in order to give it orphan pins and invalidate any // connections that will no longer be valid if (NewFName == CurrentNode->GetVarName()) { UEdGraphPin* ValuePin = CurrentNode->GetValuePin(); ValuePin->Modify(); CurrentNode->Modify(); // Make the old pin an orphan and add a new pin of the proper type UEdGraphPin* NewPin = CurrentNode->CreatePin( ValuePin->Direction, ValuePin->PinType.PinCategory, ValuePin->PinType.PinSubCategory, Widget->WidgetGeneratedByClass.Get(), // This generated object is what needs to patched up NewFName ); ValuePin->bOrphanedPin = true; } } } } } #endif // Replace the Desired focus Widget name if it match the renamed widget ReplaceDesiredFocus(BlueprintEditor, OldObjectName, NewFName); // Find and update all binding references in the widget blueprint for ( FDelegateEditorBinding& Binding : Blueprint->Bindings ) { if ( Binding.ObjectName == OldNameStr ) { Binding.ObjectName = NewNameStr; } } // Update widget blueprint names for( UWidgetAnimation* WidgetAnimation : Blueprint->Animations ) { for( FWidgetAnimationBinding& AnimBinding : WidgetAnimation->AnimationBindings ) { if( AnimBinding.WidgetName == OldObjectName ) { AnimBinding.WidgetName = NewFName; WidgetAnimation->MovieScene->Modify(); if (AnimBinding.SlotWidgetName == NAME_None) { FMovieScenePossessable* Possessable = WidgetAnimation->MovieScene->FindPossessable(AnimBinding.AnimationGuid); if (Possessable) { Possessable->SetName(NewFName.ToString()); } } else { break; } } } } // Update any explicit widget bindings. Blueprint->WidgetTree->ForEachWidget([OldObjectName, NewFName](UWidget* Widget) { if (Widget->Navigation) { Widget->Navigation->SetFlags(RF_Transactional); Widget->Navigation->Modify(); Widget->Navigation->TryToRenameBinding(OldObjectName, NewFName); } }); // If we use Components, make sure to remane the target. FUIComponentUtils::OnWidgetRenamed(BlueprintEditor, Blueprint, OldObjectName, NewFName); // Validate child blueprints and adjust variable names to avoid a potential name collision FBlueprintEditorUtils::ValidateBlueprintChildVariables(Blueprint, NewFName); // Refresh references and flush editors FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); // Update Variable References and // Update Event References to member variables FBlueprintEditorUtils::ReplaceVariableReferences(Blueprint, OldObjectName, NewFName); bRenamed = true; } return bRenamed; } void FWidgetBlueprintEditorUtils::CreateWidgetContextMenu(FMenuBuilder& MenuBuilder, TSharedRef BlueprintEditor, FVector2D TargetLocation) { BlueprintEditor->PasteDropLocation = TargetLocation; TSet Widgets = BlueprintEditor->GetSelectedWidgets(); UWidgetBlueprint* BP = BlueprintEditor->GetWidgetBlueprintObj(); MenuBuilder.BeginSection("Edit", LOCTEXT("Edit", "Edit")); { MenuBuilder.PushCommandList(BlueprintEditor->DesignerCommandList.ToSharedRef()); { MenuBuilder.AddMenuEntry(FGenericCommands::Get().Cut); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Copy); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Paste); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Duplicate); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete); // Insert "Find References" sub-menu here MenuBuilder.AddSubMenu( LOCTEXT("FindReferences_Label", "Find References"), LOCTEXT("FindReferences_Tooltip", "Options for finding references to class members"), FNewMenuDelegate::CreateStatic(&FGraphEditorCommands::BuildFindReferencesMenu), false, FSlateIcon() ); } MenuBuilder.PopCommandList(); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Rename); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("Actions"); { MenuBuilder.AddMenuEntry( LOCTEXT( "EditBlueprint_Label", "Edit Widget Blueprint..." ), LOCTEXT( "EditBlueprint_Tooltip", "Open the selected Widget Blueprint(s) for edit." ), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic( &FWidgetBlueprintEditorUtils::ExecuteOpenSelectedWidgetsForEdit, Widgets ), FCanExecuteAction(), FIsActionChecked(), FIsActionButtonVisible::CreateStatic( &FWidgetBlueprintEditorUtils::CanOpenSelectedWidgetsForEdit, Widgets ) ) ); if (!FWidgetBlueprintEditorUtils::IsAnySelectedWidgetLocked(Widgets)) { MenuBuilder.AddSubMenu( LOCTEXT("WidgetTree_WrapWith", "Wrap With..."), LOCTEXT("WidgetTree_WrapWithToolTip", "Wraps the currently selected widgets inside of another container widget"), FNewMenuDelegate::CreateStatic(&FWidgetBlueprintEditorUtils::BuildWrapWithMenu, BlueprintEditor, BP, Widgets) ); if (Widgets.Num() == 1) { MenuBuilder.AddSubMenu( LOCTEXT("WidgetTree_ReplaceWith", "Replace With..."), LOCTEXT("WidgetTree_ReplaceWithToolTip", "Replaces the currently selected widget, with another widget"), FNewMenuDelegate::CreateStatic(&FWidgetBlueprintEditorUtils::BuildReplaceWithMenu, BlueprintEditor, BP, Widgets) ); } } } MenuBuilder.EndSection(); IUMGEditorModule& EditorModule = FModuleManager::LoadModuleChecked("UMGEditor"); const TArrayView> ContextMenuExtensions = EditorModule.GetWidgetContextMenuExtensibilityManager()->GetExtensions(); for (const TSharedPtr& ContextMenuExtension : ContextMenuExtensions) { ContextMenuExtension->ExtendContextMenu(MenuBuilder, BlueprintEditor, TargetLocation); } } void FWidgetBlueprintEditorUtils::ExecuteOpenSelectedWidgetsForEdit( TSet SelectedWidgets ) { for ( auto& Widget : SelectedWidgets ) { GEditor->GetEditorSubsystem()->OpenEditorForAsset( Widget.GetTemplate()->GetClass()->ClassGeneratedBy ); } } bool FWidgetBlueprintEditorUtils::CanOpenSelectedWidgetsForEdit( TSet SelectedWidgets ) { bool bCanOpenAllForEdit = SelectedWidgets.Num() > 0; for ( auto& Widget : SelectedWidgets ) { auto Blueprint = Widget.GetTemplate()->GetClass()->ClassGeneratedBy; if ( !Blueprint || !Blueprint->IsA( UWidgetBlueprint::StaticClass() ) ) { bCanOpenAllForEdit = false; break; } } return bCanOpenAllForEdit; } void FWidgetBlueprintEditorUtils::DeleteWidgets(TSharedRef BlueprintEditor, UWidgetBlueprint* Blueprint, TSet Widgets, bool bSilentDelete /*=false*/) { DeleteWidgets(Blueprint, ResolveWidgetTemplates(Widgets), bSilentDelete ? EDeleteWidgetWarningType::DeleteSilently : EDeleteWidgetWarningType::WarnAndAskUser); } void FWidgetBlueprintEditorUtils::DeleteWidgets(UWidgetBlueprint* Blueprint, TSet Widgets, EDeleteWidgetWarningType WarningType) { if ( Widgets.Num() > 0 ) { // Check if the widgets are used in the graph FScopedTransaction Transaction(LOCTEXT("RemoveWidget", "Remove Widget")); TArray UsedVariables; TArray WidgetNames; const bool bIncludeChildrenVariables = true; FindUsedVariablesForWidgets(Widgets, Blueprint, UsedVariables, WidgetNames, bIncludeChildrenVariables); if (WarningType == EDeleteWidgetWarningType::WarnAndAskUser && UsedVariables.Num()!= 0 && !ShouldContinueDeleteOperation(Blueprint, WidgetNames)) { Transaction.Cancel(); return; } Blueprint->WidgetTree->SetFlags(RF_Transactional); Blueprint->WidgetTree->Modify(); Blueprint->Modify(); bool bRemoved = false; for (UWidget* Item : Widgets) { UWidget* WidgetTemplate = Item; WidgetTemplate->SetFlags(RF_Transactional); const FName WidgetName = WidgetTemplate->GetFName(); // Find and update all binding references in the widget blueprint for (int32 BindingIndex = Blueprint->Bindings.Num() - 1; BindingIndex >= 0; BindingIndex--) { FDelegateEditorBinding& Binding = Blueprint->Bindings[BindingIndex]; if (Binding.ObjectName == WidgetTemplate->GetName()) { Blueprint->Bindings.RemoveAt(BindingIndex); } } // Modify the widget's parent UPanelWidget* Parent = WidgetTemplate->GetParent(); if ( Parent ) { Parent->SetFlags(RF_Transactional); Parent->Modify(); } // Modify the widget being removed. WidgetTemplate->Modify(); bRemoved |= Blueprint->WidgetTree->RemoveWidget(WidgetTemplate); // If the widget we're removing doesn't have a parent it may be rooted in a named slot, // so check there as well. if ( WidgetTemplate->GetParent() == nullptr ) { bRemoved |= FindAndRemoveNamedSlotContent(WidgetTemplate, Blueprint->WidgetTree); } if (UsedVariables.Contains(WidgetTemplate)) { FBlueprintEditorUtils::RemoveVariableNodes(Blueprint, WidgetTemplate->GetFName()); } // Rename the Desired Focus that fit the Widget Deleted ReplaceDesiredFocus(Blueprint, WidgetTemplate->GetFName(), FName()); // Rename the removed widget to the transient package so that it doesn't conflict with future widgets sharing the same name. WidgetTemplate->Rename(nullptr, GetTransientPackage()); // Deletion can sometimes happen from replacing a widget with another one with the same name, so only delete the variable data if we no longer have a widget with the same name const bool bHasWidgetWithSameName = Blueprint->GetAllSourceWidgets().ContainsByPredicate([WidgetName](const UWidget* Widget) { return WidgetName == Widget->GetFName(); }); if (!bHasWidgetWithSameName) { Blueprint->OnVariableRemoved(WidgetName); } // Rename all child widgets as well, to the transient package so that they don't conflict with future widgets sharing the same name. TArray ChildWidgets; UWidgetTree::GetChildWidgets(WidgetTemplate, ChildWidgets); for ( UWidget* Widget : ChildWidgets ) { const FName ChildWidgetName = Widget->GetFName(); Widget->SetFlags(RF_Transactional); Widget->Modify(); if (UsedVariables.Contains(Widget)) { FBlueprintEditorUtils::RemoveVariableNodes(Blueprint, Widget->GetFName()); } Widget->Rename(nullptr, GetTransientPackage()); // Deletion can sometimes happen from replacing a widget with another one with the same name, so only delete the variable data if we no longer have a widget with the same name const bool bHasChildWidgetWithSameName = Blueprint->GetAllSourceWidgets().ContainsByPredicate([ChildWidgetName](const UWidget* Widget) { return ChildWidgetName == Widget->GetFName(); }); if (!bHasChildWidgetWithSameName) { Blueprint->OnVariableRemoved(ChildWidgetName); } } } //TODO UMG There needs to be an event for widget removal so that caches can be updated, and selection if ( bRemoved ) { FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); } } } void FWidgetBlueprintEditorUtils::FindUsedVariablesForWidgets(const TSet& Widgets, const UWidgetBlueprint* BP, TArray& UsedVariables, TArray& WidgetNames, bool bIncludeVariablesOnChildren) { TSet AllWidgets; AllWidgets.Reserve(Widgets.Num()); for (UWidget* Item : Widgets) { AllWidgets.Add(Item); if (bIncludeVariablesOnChildren) { TArray ChildWidgets; UWidgetTree::GetChildWidgets(Item, ChildWidgets); AllWidgets.Append(ChildWidgets); } } for (UWidget* Widget : AllWidgets) { if (FBlueprintEditorUtils::IsVariableUsed(BP, Widget->GetFName())) { WidgetNames.Add(FText::FromName(Widget->GetFName())); UsedVariables.Add(Widget); } } } bool FWidgetBlueprintEditorUtils::ShouldContinueDeleteOperation(UWidgetBlueprint* BP, const TArray& WidgetNames) { // If the Widget is used in the graph ask the user before we continue. if (WidgetNames.Num()) { FText ConfirmDelete = FText::Format(LOCTEXT("ConfirmDeleteVariableInUse", "One or more widgets are in use in the graph! Do you really want to delete them? \n\n {0}"), FText::Join(LOCTEXT("ConfirmDeleteVariableInUsedDelimiter", " \n "), WidgetNames)); // Warn the user that this may result in data loss FSuppressableWarningDialog::FSetupInfo Info(ConfirmDelete, LOCTEXT("DeleteVar", "Delete widgets"), "DeleteWidgetsInUse_Warning"); Info.ConfirmText = LOCTEXT("DeleteVariable_Yes", "Yes"); Info.CancelText = LOCTEXT("DeleteVariable_No", "No"); FSuppressableWarningDialog DeleteVariableInUse(Info); if (DeleteVariableInUse.ShowModal() == FSuppressableWarningDialog::Cancel) { return false; } } return true; } bool FWidgetBlueprintEditorUtils::ShouldContinueReplaceOperation(UWidgetBlueprint* BP, const TArray& WidgetNames) { // If the Widget is used in the graph ask the user before we continue. if (WidgetNames.Num()) { FText ConfirmDelete = FText::Format(LOCTEXT("ConfirmReplaceWidgetWithVariableInUse", "One or more widgets you want to replace are in use in the graph! Do you really want to replace them? \n\n {0}"), FText::Join(LOCTEXT("ConfirmDeleteVariableInUsedDelimiter", " \n "), WidgetNames)); // Warn the user that this may result in data loss FSuppressableWarningDialog::FSetupInfo Info(ConfirmDelete, LOCTEXT("ReplaceWidgetVar", "Replace widgets"), "ReaplaceWidgetsInUse_Warning"); Info.ConfirmText = LOCTEXT("ReplaceWidget_Yes", "Yes"); Info.CancelText = LOCTEXT("ReplaceWidget_No", "No"); FSuppressableWarningDialog DeleteVariableInUse(Info); if (DeleteVariableInUse.ShowModal() == FSuppressableWarningDialog::Cancel) { return false; } } return true; } TScriptInterface FWidgetBlueprintEditorUtils::FindNamedSlotHostForContent(UWidget* WidgetTemplate, UWidgetTree* WidgetTree) { // If the named slot comes from a parent widget class, the WidgetTree will be the SlotHost TArray SlotNames; WidgetTree->GetSlotNames(SlotNames); for (FName SlotName : SlotNames) { if (UWidget* SlotContent = WidgetTree->GetContentForSlot(SlotName)) { if (SlotContent == WidgetTemplate) { return WidgetTree; } } } return TScriptInterface(FindNamedSlotHostWidgetForContent(WidgetTemplate, WidgetTree)); } UWidget* FWidgetBlueprintEditorUtils::FindNamedSlotHostWidgetForContent(UWidget* WidgetTemplate, UWidgetTree* WidgetTree) { UWidget* HostWidget = nullptr; WidgetTree->ForEachWidget([&](UWidget* Widget) { if (HostWidget != nullptr) { return; } if (INamedSlotInterface* NamedSlotHost = Cast(Widget)) { TArray SlotNames; NamedSlotHost->GetSlotNames(SlotNames); for (FName SlotName : SlotNames) { if (UWidget* SlotContent = NamedSlotHost->GetContentForSlot(SlotName)) { if (SlotContent == WidgetTemplate) { HostWidget = Widget; } } } } }); return HostWidget; } void FWidgetBlueprintEditorUtils::FindAllAncestorNamedSlotHostWidgetsForContent(TArray& OutSlotHostWidgets, UWidget* WidgetTemplate, TSharedRef BlueprintEditor) { OutSlotHostWidgets.Empty(); UUserWidget* Preview = BlueprintEditor->GetPreview(); UWidgetBlueprint* WidgetBP = BlueprintEditor->GetWidgetBlueprintObj(); UWidgetTree* WidgetTree = (WidgetBP != nullptr) ? ToRawPtr(WidgetBP->WidgetTree) : nullptr; if (Preview != nullptr && WidgetTree != nullptr) { // Find the first widget up the chain with a null parent, they're the only candidates for this approach. while (WidgetTemplate && WidgetTemplate->GetParent()) { WidgetTemplate = WidgetTemplate->GetParent(); } UWidget* SlotHostWidget = FindNamedSlotHostWidgetForContent(WidgetTemplate, WidgetTree); while (SlotHostWidget != nullptr) { UWidget* SlotWidget = Preview->GetWidgetFromName(SlotHostWidget->GetFName()); FWidgetReference WidgetRef; if (SlotWidget != nullptr) { WidgetRef = BlueprintEditor->GetReferenceFromPreview(SlotWidget); if (WidgetRef.IsValid()) { OutSlotHostWidgets.Add(WidgetRef); } } WidgetTemplate = WidgetRef.GetTemplate(); SlotHostWidget = nullptr; if (WidgetTemplate != nullptr) { // Find the first widget up the chain with a null parent, they're the only candidates for this approach. while (WidgetTemplate->GetParent()) { WidgetTemplate = WidgetTemplate->GetParent(); } SlotHostWidget = FindNamedSlotHostWidgetForContent(WidgetRef.GetTemplate(), WidgetTree); } } } } bool FWidgetBlueprintEditorUtils::RemoveNamedSlotHostContent(UWidget* WidgetTemplate, TScriptInterface NamedSlotHost) { return ReplaceNamedSlotHostContent(WidgetTemplate, NamedSlotHost, nullptr); } bool FWidgetBlueprintEditorUtils::ReplaceNamedSlotHostContent(UWidget* WidgetTemplate, TScriptInterface NamedSlotHost, UWidget* NewContentWidget) { TArray SlotNames; NamedSlotHost->GetSlotNames(SlotNames); for (FName SlotName : SlotNames) { if (UWidget* SlotContent = NamedSlotHost->GetContentForSlot(SlotName)) { if (SlotContent == WidgetTemplate) { NamedSlotHost.GetObject()->Modify(); if (UPanelWidget* NamedSlot = WidgetTemplate->GetParent()) { // Make sure we also mark the named slot as modified to properly track changes in it. NamedSlot->Modify(); } if (NewContentWidget) { NewContentWidget->Modify(); if (UPanelWidget* Parent = NewContentWidget->GetParent()) { Parent->Modify(); NewContentWidget->RemoveFromParent(); } } NamedSlotHost->SetContentForSlot(SlotName, NewContentWidget); return true; } } } return false; } bool FWidgetBlueprintEditorUtils::FindAndRemoveNamedSlotContent(UWidget* WidgetTemplate, UWidgetTree* WidgetTree) { UWidget* NamedSlotHostWidget = FindNamedSlotHostWidgetForContent(WidgetTemplate, WidgetTree); if (TScriptInterface NamedSlotHost = TScriptInterface(NamedSlotHostWidget) ) { NamedSlotHostWidget->Modify(); return RemoveNamedSlotHostContent(WidgetTemplate, NamedSlotHost); } return false; } void FWidgetBlueprintEditorUtils::BuildWrapWithMenu(FMenuBuilder& Menu, TSharedRef BlueprintEditor, UWidgetBlueprint* BP, TSet Widgets) { TArray WrapperClasses; for ( TObjectIterator ClassIt; ClassIt; ++ClassIt ) { UClass* WidgetClass = *ClassIt; if ( FWidgetBlueprintEditorUtils::IsUsableWidgetClass(WidgetClass, BlueprintEditor) ) { if ( WidgetClass->IsChildOf(UPanelWidget::StaticClass()) && !WidgetClass->HasAnyClassFlags(CLASS_HideDropDown) ) { WrapperClasses.Add(WidgetClass); } } } WrapperClasses.Sort([] (UClass& Lhs, UClass& Rhs) { return Lhs.GetDisplayNameText().CompareTo(Rhs.GetDisplayNameText()) < 0; }); Menu.BeginSection("WrapWith", LOCTEXT("WidgetTree_WrapWith", "Wrap With...")); { for ( UClass* WrapperClass : WrapperClasses ) { Menu.AddMenuEntry( WrapperClass->GetDisplayNameText(), FText::GetEmpty(), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic(&FWidgetBlueprintEditorUtils::WrapWidgets, BlueprintEditor, BP, Widgets, WrapperClass), FCanExecuteAction() )); } } Menu.EndSection(); } void FWidgetBlueprintEditorUtils::WrapWidgets(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, TSet Widgets, UClass* WidgetClass) { const FScopedTransaction Transaction(LOCTEXT("WrapWidgets", "Wrap Widgets")); TSharedPtr Template = MakeShareable(new FWidgetTemplateClass(WidgetClass)); // When selecting multiple widgets, we only want to create a new wrapping widget around the root-most set of widgets // So find any that children of other selected widgets, and skip them (because their parents will be wrapped) TSet WidgetsToRemove; for (FWidgetReference& Item : Widgets) { int32 OutIndex; UPanelWidget* CurrentParent = BP->WidgetTree->FindWidgetParent(Item.GetTemplate(), OutIndex); for (FWidgetReference& OtherItem : Widgets) { if (OtherItem.GetTemplate() == CurrentParent) { WidgetsToRemove.Add(Item); break; } } } for (FWidgetReference& Item : WidgetsToRemove) { Widgets.Remove(Item); } WidgetsToRemove.Empty(); // Old Parent -> New Parent Map TMap OldParentToNewParent; for (FWidgetReference& Item : Widgets) { int32 OutIndex; UWidget* Widget = Item.GetTemplate(); UPanelWidget* CurrentParent = BP->WidgetTree->FindWidgetParent(Widget, OutIndex); TScriptInterface NamedSlotHost = FindNamedSlotHostForContent(Widget, BP->WidgetTree); // If the widget doesn't currently have a slot or parent, and isn't the root, ignore it. if (NamedSlotHost == nullptr && CurrentParent == nullptr && Widget != BP->WidgetTree->RootWidget) { continue; } Widget->Modify(); BP->WidgetTree->SetFlags(RF_Transactional); BP->WidgetTree->Modify(); if (NamedSlotHost) { // If this is a named slot, we need to properly remove and reassign the slot content if (UObject* NamedSlotObject = NamedSlotHost.GetObject()) { NamedSlotObject->SetFlags(RF_Transactional); NamedSlotObject->Modify(); UPanelWidget* NewSlotContents = CastChecked(Template->Create(BP->WidgetTree)); NewSlotContents->SetDesignerFlags(BlueprintEditor->GetCurrentDesignerFlags()); BP->OnVariableAdded(NewSlotContents->GetFName()); FWidgetBlueprintEditorUtils::ReplaceNamedSlotHostContent(Widget, NamedSlotHost, NewSlotContents); NewSlotContents->AddChild(Widget); } } else if (CurrentParent) { UPanelWidget*& NewWrapperWidget = OldParentToNewParent.FindOrAdd(CurrentParent); if (NewWrapperWidget == nullptr || !NewWrapperWidget->CanAddMoreChildren()) { NewWrapperWidget = CastChecked(Template->Create(BP->WidgetTree)); NewWrapperWidget->SetDesignerFlags(BlueprintEditor->GetCurrentDesignerFlags()); BP->OnVariableAdded(NewWrapperWidget->GetFName()); CurrentParent->SetFlags(RF_Transactional); CurrentParent->Modify(); CurrentParent->ReplaceChildAt(OutIndex, NewWrapperWidget); } if (NewWrapperWidget != nullptr && NewWrapperWidget->CanAddMoreChildren()) { NewWrapperWidget->Modify(); NewWrapperWidget->AddChild(Widget); } } else { UPanelWidget* NewRootContents = CastChecked(Template->Create(BP->WidgetTree)); NewRootContents->SetDesignerFlags(BlueprintEditor->GetCurrentDesignerFlags()); BP->OnVariableAdded(NewRootContents->GetFName()); BP->WidgetTree->RootWidget = NewRootContents; NewRootContents->AddChild(Widget); } } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); } void FWidgetBlueprintEditorUtils::BuildReplaceWithMenu(FMenuBuilder& Menu, TSharedRef BlueprintEditor, UWidgetBlueprint* BP, TSet Widgets) { Menu.BeginSection("ReplaceWith", LOCTEXT("WidgetTree_ReplaceWith", "Replace With...")); { if ( Widgets.Num() == 1 ) { FWidgetReference Widget = *Widgets.CreateIterator(); UClass* WidgetClass = Widget.GetTemplate()->GetClass(); TWeakObjectPtr TemplateWidget = BlueprintEditor->GetSelectedTemplate(); FAssetData SelectedUserWidget = BlueprintEditor->GetSelectedUserWidget(); if (TemplateWidget.IsValid() || SelectedUserWidget.GetSoftObjectPath().IsValid() ) { Menu.AddMenuEntry( FText::Format(LOCTEXT("WidgetTree_ReplaceWithSelection", "Replace With {0}"), FText::FromString(TemplateWidget.IsValid() ? TemplateWidget->GetName() : SelectedUserWidget.AssetName.ToString())), FText::Format(LOCTEXT("WidgetTree_ReplaceWithSelectionToolTip", "Replace this widget with a {0}"), FText::FromString(TemplateWidget.IsValid() ? TemplateWidget->GetName() : SelectedUserWidget.AssetName.ToString())), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic(&FWidgetBlueprintEditorUtils::ReplaceWidgetWithSelectedTemplate, BlueprintEditor, BP, Widget), FCanExecuteAction::CreateStatic(&FWidgetBlueprintEditorUtils::CanBeReplacedWithTemplate, BlueprintEditor, BP, Widget) )); Menu.AddMenuSeparator(); } if ( WidgetClass->IsChildOf(UPanelWidget::StaticClass()) && Cast(Widget.GetTemplate())->GetChildrenCount() == 1 ) { Menu.AddMenuEntry( LOCTEXT("ReplaceWithChild", "Replace With Child"), LOCTEXT("ReplaceWithChildTooltip", "Remove this widget and insert the children of this widget into the parent."), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic(&FWidgetBlueprintEditorUtils::ReplaceWidgetWithChildren, BlueprintEditor, BP, Widget), FCanExecuteAction() )); Menu.AddMenuSeparator(); } if (TScriptInterface NamedSlotHost = TScriptInterface(Widget.GetTemplate())) { TArray SlotNames; NamedSlotHost->GetSlotNames(SlotNames); for (const FName& SlotName : SlotNames) { const FText SlotNameTxt = FText::FromString(SlotName.ToString()); if (UWidget* Content = NamedSlotHost->GetContentForSlot(SlotName)) { Menu.AddMenuEntry( FText::Format(LOCTEXT("ReplaceWithNamedSlot", "Replace With '{0}'"), SlotNameTxt), FText::Format(LOCTEXT("ReplaceWithNamedSlotTooltip", "Remove this widget and insert '{0}' content into the parent."), SlotNameTxt), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic(&FWidgetBlueprintEditorUtils::ReplaceWidgetWithNamedSlot, BlueprintEditor, BP, Widget, SlotName), FCanExecuteAction() )); } } Menu.AddMenuSeparator(); } } TArray ReplacementClasses; for ( TObjectIterator ClassIt; ClassIt; ++ClassIt ) { UClass* WidgetClass = *ClassIt; if ( FWidgetBlueprintEditorUtils::IsUsableWidgetClass(WidgetClass, BlueprintEditor) ) { if ( WidgetClass->IsChildOf(UPanelWidget::StaticClass()) && !WidgetClass->HasAnyClassFlags(CLASS_HideDropDown) ) { // Only allow replacement with panels that accept multiple children if ( WidgetClass->GetDefaultObject()->CanHaveMultipleChildren() ) { ReplacementClasses.Add(WidgetClass); } } } } ReplacementClasses.Sort([] (UClass& Lhs, UClass& Rhs) { return Lhs.GetDisplayNameText().CompareTo(Rhs.GetDisplayNameText()) < 0; }); for ( UClass* ReplacementClass : ReplacementClasses ) { Menu.AddMenuEntry( ReplacementClass->GetDisplayNameText(), FText::GetEmpty(), FSlateIcon(), FUIAction( FExecuteAction::CreateStatic(&FWidgetBlueprintEditorUtils::ReplaceWidgets, BP, ResolveWidgetTemplates(Widgets), ReplacementClass, EReplaceWidgetNamingMethod::MaintainNameAndReferences) )); } } Menu.EndSection(); } bool FWidgetBlueprintEditorUtils::IsDesiredFocusWidget(TSharedRef BlueprintEditor, UWidget* Widget) { return IsDesiredFocusWidget(BlueprintEditor->GetWidgetBlueprintObj(), Widget); } bool FWidgetBlueprintEditorUtils::IsDesiredFocusWidget(UWidgetBlueprint* Blueprint, UWidget* Widget) { if (Blueprint && Blueprint->GeneratedClass && Widget) { if (UUserWidget* WidgetCDO = Blueprint->GeneratedClass->GetDefaultObject()) { return (WidgetCDO && WidgetCDO->GetDesiredFocusWidgetName() == Widget->GetFName()); } } return false; } void FWidgetBlueprintEditorUtils::ReplaceWidgetWithSelectedTemplate(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, FWidgetReference Widget) { // @Todo: Needs to deal with bound object in animation tracks UWidget* WidgetToReplace = Widget.GetTemplate(); if (!WidgetToReplace) { return; } UClass* ReplacementWidgetClass = BlueprintEditor->GetSelectedTemplate().Get(); if (!ReplacementWidgetClass) { ReplacementWidgetClass = BlueprintEditor->GetSelectedUserWidget().GetClass(EResolveClass::Yes); } if (!ReplacementWidgetClass) { return; } ReplaceWidgets(BP, {WidgetToReplace}, ReplacementWidgetClass, EReplaceWidgetNamingMethod::MaintainNameAndReferences); } bool FWidgetBlueprintEditorUtils::CanBeReplacedWithTemplate(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, FWidgetReference Widget) { FAssetData SelectedUserWidget = BlueprintEditor->GetSelectedUserWidget(); UWidget* ThisWidget = Widget.GetTemplate(); UPanelWidget* ExistingPanel = Cast(ThisWidget); UClass* WidgetClass = nullptr; // If selecting another widget blueprint if (SelectedUserWidget.GetSoftObjectPath().IsValid()) { if (ExistingPanel && ExistingPanel->GetChildrenCount() != 0) { return false; } if (UWidget* NewWidget = FWidgetTemplateBlueprintClass(SelectedUserWidget).Create(BP->WidgetTree)) { // If we are creating a UserWidget, check for Circular references if (UUserWidget* NewUserWidget = Cast(NewWidget)) { const bool bFreeFromCircularRefs = BP->IsWidgetFreeFromCircularReferences(NewUserWidget); NewWidget->Rename(nullptr, GetTransientPackage()); return bFreeFromCircularRefs; } WidgetClass = NewWidget->GetClass(); NewWidget->Rename(nullptr, GetTransientPackage()); } } // If we get here, the Widget selected is not a UserWidget and it's not a Blueprint. if (!WidgetClass) { WidgetClass = BlueprintEditor->GetSelectedTemplate().Get(); } // If the Widget to replace is not a Panel we can replace it with anything if (!ExistingPanel) { return true; } const bool bNewWidgetClassIsAPanel = WidgetClass->IsChildOf(UPanelWidget::StaticClass()); // If the Widget to replace is a Panel and the new widget is not, we allow to replace it only if it's empty; if (!bNewWidgetClassIsAPanel) { return ExistingPanel->GetChildrenCount() == 0; } // If the Widget to replace is a Panel that can have multiple children, we allow to replace it with a Panel that can support multiple children only. if (ExistingPanel->GetClass()->GetDefaultObject()->CanHaveMultipleChildren() && bNewWidgetClassIsAPanel) { const bool bChildAllowed = WidgetClass->GetDefaultObject()->CanHaveMultipleChildren() || ExistingPanel->GetChildrenCount() == 0; return bChildAllowed; } return true; } void FWidgetBlueprintEditorUtils::ReplaceWidgetWithChildren(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, FWidgetReference Widget) { FScopedTransaction Transaction(LOCTEXT("ReplaceWidgets", "Replace Widgets")); TSet WidgetsToDelete; WidgetsToDelete.Add(Widget); TArray UsedVariables; TArray WidgetNames; const bool bIncludeChildrenVariables = false; FindUsedVariablesForWidgets(ResolveWidgetTemplates(WidgetsToDelete), BP, UsedVariables, WidgetNames, bIncludeChildrenVariables); if (UsedVariables.Num() != 0 && !ShouldContinueReplaceOperation(BP, WidgetNames)) { Transaction.Cancel(); return; } if ( UPanelWidget* ExistingPanelTemplate = Cast(Widget.GetTemplate()) ) { UWidget* FirstChildTemplate = ExistingPanelTemplate->GetChildAt(0); ExistingPanelTemplate->SetFlags(RF_Transactional); ExistingPanelTemplate->Modify(); FirstChildTemplate->SetFlags(RF_Transactional); FirstChildTemplate->Modify(); // Look if the Widget to replace is a NamedSlot. if (TScriptInterface NamedSlotHost = FindNamedSlotHostForContent(ExistingPanelTemplate, BP->WidgetTree)) { ReplaceNamedSlotHostContent(ExistingPanelTemplate, NamedSlotHost, FirstChildTemplate); } else if (UPanelWidget* PanelParentTemplate = ExistingPanelTemplate->GetParent()) { PanelParentTemplate->Modify(); FirstChildTemplate->RemoveFromParent(); PanelParentTemplate->ReplaceChild(ExistingPanelTemplate, FirstChildTemplate); } else if ( ExistingPanelTemplate == BP->WidgetTree->RootWidget ) { FirstChildTemplate->RemoveFromParent(); BP->WidgetTree->Modify(); BP->WidgetTree->RootWidget = FirstChildTemplate; } else { Transaction.Cancel(); return; } // Delete the widget that has been replaced DeleteWidgets(BP, ResolveWidgetTemplates(WidgetsToDelete), EDeleteWidgetWarningType::DeleteSilently); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); } } void FWidgetBlueprintEditorUtils::ReplaceWidgetWithNamedSlot(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, FWidgetReference Widget, FName NamedSlot) { UWidget* WidgetTemplate = Widget.GetTemplate(); if (INamedSlotInterface* ExistingNamedSlotContainerTemplate = Cast(WidgetTemplate)) { UWidget* NamedSlotContentTemplate = ExistingNamedSlotContainerTemplate->GetContentForSlot(NamedSlot); FScopedTransaction Transaction(LOCTEXT("ReplaceWidgets", "Replace Widgets")); WidgetTemplate->SetFlags(RF_Transactional); WidgetTemplate->Modify(); NamedSlotContentTemplate->SetFlags(RF_Transactional); NamedSlotContentTemplate->Modify(); // Look if the Widget to replace is a NamedSlot. if (TScriptInterface NamedSlotHost = FindNamedSlotHostForContent(WidgetTemplate, BP->WidgetTree)) { ReplaceNamedSlotHostContent(WidgetTemplate, NamedSlotHost, NamedSlotContentTemplate); } else if (UPanelWidget* PanelParentTemplate = WidgetTemplate->GetParent()) { PanelParentTemplate->Modify(); if (TScriptInterface ContentNamedSlotHost = FindNamedSlotHostForContent(NamedSlotContentTemplate, BP->WidgetTree)) { FWidgetBlueprintEditorUtils::RemoveNamedSlotHostContent(NamedSlotContentTemplate, ContentNamedSlotHost); } PanelParentTemplate->ReplaceChild(WidgetTemplate, NamedSlotContentTemplate); } else if (WidgetTemplate == BP->WidgetTree->RootWidget) { if (UPanelWidget* Parent = NamedSlotContentTemplate->GetParent()) { Parent->Modify(); NamedSlotContentTemplate->RemoveFromParent(); } BP->WidgetTree->Modify(); BP->WidgetTree->RootWidget = NamedSlotContentTemplate; } else { Transaction.Cancel(); return; } // Remove the widget replaced DeleteWidgets(BP, {Widget.GetTemplate()}, EDeleteWidgetWarningType::WarnAndAskUser); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); } } void FWidgetBlueprintEditorUtils::ReplaceWidgets(UWidgetBlueprint* BP, TSet Widgets, UClass* WidgetClass, EReplaceWidgetNamingMethod NewWidgetNamingMethod) { FScopedTransaction Transaction(LOCTEXT("ReplaceWidgets", "Replace Widgets")); TArray UsedVariables; TArray WidgetNames; const bool bIncludeChildrenVariables = false; FindUsedVariablesForWidgets(Widgets, BP, UsedVariables, WidgetNames, bIncludeChildrenVariables); if (UsedVariables.Num() != 0 && !ShouldContinueReplaceOperation(BP, WidgetNames)) { Transaction.Cancel(); return; } TSharedPtr Template = MakeShareable(new FWidgetTemplateClass(WidgetClass)); TMap ReplacedWidgetMap; for (UWidget* Item : Widgets) { BP->WidgetTree->SetFlags(RF_Transactional); BP->WidgetTree->Modify(); UWidget* NewReplacementWidget = Template->Create(BP->WidgetTree); UWidget* WidgetToReplace = Item; // If replacing a panel widget, then it must not have children or the replacement must also be a panel widget if (UPanelWidget* ExistingPanel = Cast(WidgetToReplace)) { if (ExistingPanel->GetChildrenCount() > 0 && !NewReplacementWidget->IsA()) { continue; } } TMap ExportedProperties; ExportPropertiesToText(WidgetToReplace, ExportedProperties); ImportPropertiesFromText(NewReplacementWidget, ExportedProperties); WidgetToReplace->SetFlags(RF_Transactional); WidgetToReplace->Modify(); const FName OriginalWidgetName = WidgetToReplace->GetFName(); // Look if the Widget to replace is a NamedSlot. if (TScriptInterface NamedSlotHost = FindNamedSlotHostForContent(WidgetToReplace, BP->WidgetTree)) { const bool bDidReplace = ReplaceNamedSlotHostContent(WidgetToReplace, NamedSlotHost, NewReplacementWidget); if (!bDidReplace) { continue; } } else if (UPanelWidget* CurrentParent = WidgetToReplace->GetParent()) { CurrentParent->SetFlags(RF_Transactional); CurrentParent->Modify(); const bool bDidReplace = CurrentParent->ReplaceChild(WidgetToReplace, NewReplacementWidget); if (!bDidReplace) { continue; } } else if (WidgetToReplace == BP->WidgetTree->RootWidget) { BP->WidgetTree->RootWidget = NewReplacementWidget; } else { continue; } NewReplacementWidget->SetFlags(RF_Transactional); NewReplacementWidget->Modify(); if (UPanelWidget* ExistingPanel = Cast(WidgetToReplace)) { if (UPanelWidget* NewReplacementPanelWidget = Cast(NewReplacementWidget)) { while (ExistingPanel->GetChildrenCount() > 0) { UWidget* Widget = ExistingPanel->GetChildAt(0); Widget->SetFlags(RF_Transactional); Widget->Modify(); NewReplacementPanelWidget->AddChild(Widget); } } } // We need to check before replacing because the Widget might be deleted, reseting the DesiredFocus const bool bReplacingDesiredFocus = IsDesiredFocusWidget(BP, WidgetToReplace); FString ReplaceName = WidgetToReplace->GetName(); const bool bCanKeepName = (NewWidgetNamingMethod == EReplaceWidgetNamingMethod::MaintainNameAndReferencesForUnmatchingClass) || (!WidgetToReplace->IsGeneratedName() && NewWidgetNamingMethod == EReplaceWidgetNamingMethod::MaintainNameAndReferences && ((WidgetToReplace->IsA() && NewReplacementWidget->IsA()) || WidgetToReplace->IsA(NewReplacementWidget->GetClass()) || NewReplacementWidget->IsA(WidgetToReplace->GetClass()))); // Rename the removed widget to the transient package so that it doesn't conflict with the new widget if we try to keep the same name. FName TrashName = MakeUniqueObjectName(GetTransientPackage(), WidgetToReplace->GetClass(), *FString::Printf(TEXT("TRASH_%s"), *WidgetToReplace->GetName())); WidgetToReplace->Rename(*TrashName.ToString(), GetTransientPackage()); // Rename the new Widget to maintain the current name if it's not a generic name if (NewWidgetNamingMethod == EReplaceWidgetNamingMethod::MaintainNameAndReferences || NewWidgetNamingMethod == EReplaceWidgetNamingMethod::MaintainNameAndReferencesForUnmatchingClass) { if (bCanKeepName) { ReplaceName = FindNextValidName(BP->WidgetTree, ReplaceName); NewReplacementWidget->Rename(*ReplaceName, BP->WidgetTree); } // Preserve references to the widget if we haven't kept the same name if (OriginalWidgetName != NewReplacementWidget->GetFName()) { BP->OnVariableRenamed(OriginalWidgetName, NewReplacementWidget->GetFName()); } // Even if the name hasn't changed, we need to replace references since the type might have changed ReplacedWidgetMap.Add(OriginalWidgetName, NewReplacementWidget->GetFName()); } else if (NewReplacementWidget->GetFName() != OriginalWidgetName) { BP->OnVariableRemoved(OriginalWidgetName); BP->OnVariableAdded(NewReplacementWidget->GetFName()); } // Delete the widget that has been replaced DeleteWidgets(BP, {Item}, EDeleteWidgetWarningType::DeleteSilently); if (bReplacingDesiredFocus) { SetDesiredFocus(BP, NewReplacementWidget->GetFName()); } } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); for (const TPair& RenamedWidgets : ReplacedWidgetMap) { FBlueprintEditorUtils::ReplaceVariableReferences(BP, RenamedWidgets.Key, RenamedWidgets.Value); } } void FWidgetBlueprintEditorUtils::CutWidgets(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, TSet Widgets) { CopyWidgets(BP, Widgets); DeleteWidgets(BP, ResolveWidgetTemplates(Widgets), EDeleteWidgetWarningType::WarnAndAskUser); } void FWidgetBlueprintEditorUtils::CopyWidgets(UWidgetBlueprint* BP, TSet Widgets) { FString ExportedText = CopyWidgetsInternal(BP, Widgets); FPlatformApplicationMisc::ClipboardCopy(*ExportedText); } FString FWidgetBlueprintEditorUtils::CopyWidgetsInternal(UWidgetBlueprint* BP, TSet Widgets) { TSet TemplateWidgets; // Convert the set of widget references into the list of widget templates we're going to copy. for ( const FWidgetReference& Widget : Widgets ) { UWidget* TemplateWidget = Widget.GetTemplate(); TemplateWidgets.Add(TemplateWidget); } TArray FinalWidgets; // Pair down copied widgets to the legitimate root widgets, if they're parent is not already // in the set we're planning to copy, then keep them in the list, otherwise remove widgets that // will already be handled when their parent copies into the array. for ( UWidget* TemplateWidget : TemplateWidgets ) { bool bFoundParent = false; // See if the widget already has a parent in the set we're copying. for ( UWidget* PossibleParent : TemplateWidgets ) { if ( PossibleParent != TemplateWidget ) { if ( TemplateWidget->IsChildOf(PossibleParent) ) { bFoundParent = true; break; } } } if ( !bFoundParent ) { FinalWidgets.Add(TemplateWidget); UWidgetTree::GetChildWidgets(TemplateWidget, FinalWidgets); } } FString ExportedText; FWidgetBlueprintEditorUtils::ExportWidgetsToText(FinalWidgets, /*out*/ ExportedText); return ExportedText; } TArray FWidgetBlueprintEditorUtils::DuplicateWidgets(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, TSet Widgets) { TArray DuplicatedWidgets; FWidgetReference ParentWidgetRef = Widgets.Num() > 0 ? *Widgets.CreateIterator() : FWidgetReference(); FName SlotName = NAME_None; TOptional NamedSlotSelection = BlueprintEditor->GetSelectedNamedSlot(); if (NamedSlotSelection.IsSet()) { ParentWidgetRef = NamedSlotSelection->NamedSlotHostWidget; SlotName = NamedSlotSelection->SlotName; } if (ParentWidgetRef.IsValid()) { FString ExportedText = CopyWidgetsInternal(BP, Widgets); FScopedTransaction Transaction(FGenericCommands::Get().Duplicate->GetDescription()); bool TransactionSuccesful = true; DuplicatedWidgets = PasteWidgetsInternal(BlueprintEditor, BP, ExportedText, ParentWidgetRef, SlotName, FVector2D::ZeroVector, true, TransactionSuccesful); if (!TransactionSuccesful) { BlueprintEditor->LogSimpleMessage(LOCTEXT("PasteWidgetsCancel", "Paste operation on widget cancelled.")); Transaction.Cancel(); } } return DuplicatedWidgets; } UUserWidget* FWidgetBlueprintEditorUtils::CreateUserWidgetFromBlueprint(UObject* Outer, UWidgetBlueprint* BP, const FCreateWidgetFromBlueprintParams& Params) { check(Outer); check(BP); UUserWidget* CreatedUserWidget = nullptr; // Create the Widget, we have to do special swapping out of the widget tree. { // Assign the outer to the game instance if it exists, otherwise use the world { FMakeClassSpawnableOnScope TemporarilySpawnable(BP->GeneratedClass); CreatedUserWidget = NewObject(Outer, BP->GeneratedClass); } // The preview widget should not be transactional. CreatedUserWidget->ClearFlags(RF_Transactional); // Establish the widget as being in design time before initializing and before duplication // (so that IsDesignTime is reliable within both calls to Initialize) // The preview widget is also the outer widget that will update all child flags CreatedUserWidget->SetDesignerFlags(Params.FlagsToApply); if (ULocalPlayer* Player = Params.LocalPlayer) { CreatedUserWidget->SetPlayerContext(FLocalPlayerContext(Player)); } UWidgetTree* LatestWidgetTree = FWidgetBlueprintEditorUtils::FindLatestWidgetTree(BP, CreatedUserWidget); TMap SortedNamedSlotContentToMerge; UWidgetBlueprint* WidgetBlueprintIterator = BP; TArray> NamedSlotContentToMergeArray; while (WidgetBlueprintIterator) { TArray SlotNames; WidgetBlueprintIterator->WidgetTree->GetSlotNames(SlotNames); // We iterate widget blueprints from child to parent, but we need the final namedslot array to be sorted from parent to child. // Here, we iterate the slot names in reverse to maintain the order of namedslots per widget blueprint once the final array is reversed. for (int32 Index = SlotNames.Num() - 1; Index >= 0; Index--) { FName SlotName = SlotNames[Index]; if (UWidget* Content = WidgetBlueprintIterator->WidgetTree->GetContentForSlot(SlotName)) { NamedSlotContentToMergeArray.Add(TTuple(SlotName, Content)); } } WidgetBlueprintIterator = Cast(WidgetBlueprintIterator->GeneratedClass->GetSuperClass()->ClassGeneratedBy); } // We iterate the array in reverse so that the final SortedNamedSlotContentToMerge map ends up sorted from outermost namedslot to innermost. for (int32 Index = NamedSlotContentToMergeArray.Num() - 1; Index >= 0; Index--) { TTuple& Element = NamedSlotContentToMergeArray[Index]; SortedNamedSlotContentToMerge.Add(Element.Key, Element.Value); } // Update the widget tree directly to match the blueprint tree. That way the preview can update // without needing to do a full recompile. CreatedUserWidget->DuplicateAndInitializeFromWidgetTree(LatestWidgetTree, SortedNamedSlotContentToMerge); // Establish the widget as being in design time before initializing (so that IsDesignTime is reliable within Initialize) // We have to call it to make sure that all the WidgetTree had the DesignerFlags set correctly CreatedUserWidget->SetDesignerFlags(Params.FlagsToApply); } return CreatedUserWidget; } void FWidgetBlueprintEditorUtils::DestroyUserWidget(UUserWidget* UserWidget) { check(UserWidget); TWeakPtr SlateWidgetWeak = UserWidget->GetCachedWidget(); UserWidget->MarkAsGarbage(); UserWidget->ReleaseSlateResources(true); ensure(!SlateWidgetWeak.IsValid()); } bool FWidgetBlueprintEditorUtils::IsAnySelectedWidgetLocked(TSet SelectedWidgets) { for (const FWidgetReference& Widget : SelectedWidgets) { if (Widget.GetPreview()->IsLockedInDesigner()) { return true; } } return false; } bool FWidgetBlueprintEditorUtils::CanPasteWidgetsExtension(TSet SelectedWidgets) { if (!SelectedWidgets.IsEmpty()) { IUMGEditorModule& EditorModule = FModuleManager::LoadModuleChecked("UMGEditor"); const TArrayView> ClipboardExtensions = EditorModule.GetClipboardExtensibilityManager()->GetExtensions(); for (const TSharedPtr& ClipboardExtension : ClipboardExtensions) { if (ensure(ClipboardExtension.IsValid())) { for (const FWidgetReference& SelectedWidget : SelectedWidgets) { if (UWidget* TemplateWidget = SelectedWidget.GetTemplate()) { if (!ClipboardExtension->CanWidgetAcceptPaste(TemplateWidget)) { return false; } } } } } } return true; } UWidget* FWidgetBlueprintEditorUtils::GetWidgetTemplateFromDragDrop(UWidgetBlueprint* Blueprint, UWidgetTree* RootWidgetTree, TSharedPtr& DragDropOp) { UWidget* Widget = nullptr; if (!DragDropOp.IsValid()) { return nullptr; } if (DragDropOp->IsOfType()) { TSharedPtr TemplateDragDropOp = StaticCastSharedPtr(DragDropOp); Widget = TemplateDragDropOp->Template->Create(RootWidgetTree); } else if (DragDropOp->IsOfType()) { TSharedPtr AssetDragDropOp = StaticCastSharedPtr(DragDropOp); if (AssetDragDropOp->GetAssets().Num() > 0) { // Only handle first valid dragged widget, multi widget drag drop is not practically useful const FAssetData& AssetData = AssetDragDropOp->GetAssets()[0]; bool CodeClass = AssetData.AssetClassPath == FTopLevelAssetPath(TEXT("/Script/CoreUObject"), TEXT("Class")); FString ClassName = CodeClass ? AssetData.GetObjectPathString() : AssetData.AssetClassPath.ToString(); UClass* AssetClass = FindObjectChecked(nullptr, *ClassName); if (FWidgetTemplateBlueprintClass::Supports(AssetClass)) { // Allows a UMG Widget Blueprint to be dragged from the Content Browser to another Widget Blueprint...as long as we're not trying to place a // blueprint inside itself. FString BlueprintPath = Blueprint->GetPathName(); if (BlueprintPath != AssetData.GetSoftObjectPath().ToString()) { Widget = FWidgetTemplateBlueprintClass(AssetData).Create(RootWidgetTree); } } else if (CodeClass && AssetClass && AssetClass->IsChildOf(UWidget::StaticClass())) { Widget = FWidgetTemplateClass(AssetClass).Create(RootWidgetTree); } else if (FWidgetTemplateImageClass::Supports(AssetClass)) { Widget = FWidgetTemplateImageClass(AssetData).Create(RootWidgetTree); } } } // Check to make sure that this widget can be added to the current blueprint if (Cast(Widget) && !Blueprint->IsWidgetFreeFromCircularReferences(Cast(Widget))) { Widget = nullptr; } return Widget; } bool FWidgetBlueprintEditorUtils::ShouldPreventDropOnTargetExtensions(const UWidget* Target, const TSharedPtr& DragDropOp, FText& OutFailureText) { if (Target) { IUMGEditorModule& EditorModule = FModuleManager::LoadModuleChecked("UMGEditor"); const TArrayView> DragDropExtensions = EditorModule.GetWidgetDragDropExtensibilityManager()->GetExtensions(); for (const TSharedPtr& DragDropExtension : DragDropExtensions) { if (ensure(DragDropExtension.IsValid()) && DragDropExtension->ShouldPreventDropOnTarget(Target, DragDropOp)) { OutFailureText = DragDropExtension->GetDropFailureText(Target, DragDropOp); return true; } } } return false; } void FWidgetBlueprintEditorUtils::ExportWidgetsToText(TArray WidgetsToExport, /*out*/ FString& ExportedText) { // Clear the mark state for saving. UnMarkAllObjects(EObjectMark(OBJECTMARK_TagExp | OBJECTMARK_TagImp)); FStringOutputDevice Archive; // Validate all nodes are from the same scope and set all UUserWidget::WidgetTrees (and things outered to it) to be ignored TArray WidgetsToIgnore; UObject* LastOuter = nullptr; for ( UWidget* Widget : WidgetsToExport ) { // The nodes should all be from the same scope UObject* ThisOuter = Widget->GetOuter(); check((LastOuter == ThisOuter) || (LastOuter == nullptr)); LastOuter = ThisOuter; if ( UUserWidget* UserWidget = Cast(Widget) ) { if ( UserWidget->WidgetTree ) { WidgetsToIgnore.Add(UserWidget->WidgetTree); // FExportObjectInnerContext does not automatically ignore UObjects if their outer is ignored GetObjectsWithOuter(UserWidget->WidgetTree, WidgetsToIgnore); } } } const FExportObjectInnerContext Context(WidgetsToIgnore); IUMGEditorModule& EditorModule = FModuleManager::LoadModuleChecked("UMGEditor"); const TArrayView> ClipboardExtensions = EditorModule.GetClipboardExtensibilityManager()->GetExtensions(); // Get the widget blueprint containing the exported widgets UWidgetBlueprint* WidgetBlueprint = nullptr; if (WidgetsToExport.Num() > 0) { WidgetBlueprint = FWidgetBlueprintEditorUtils::GetWidgetBlueprintFromWidget(WidgetsToExport[0]); } // Export each of the selected nodes for ( UWidget* Widget : WidgetsToExport ) { UExporter::ExportToOutputDevice(&Context, Widget, nullptr, Archive, TEXT("copy"), 0, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited, false, LastOuter); // Check to see if this widget was content of another widget holding it in a named slot. if ( Widget->GetParent() == nullptr ) { for ( UWidget* ExportableWidget : WidgetsToExport ) { if ( INamedSlotInterface* NamedSlotContainer = Cast(ExportableWidget) ) { if ( NamedSlotContainer->ContainsContent(Widget) ) { continue; } } } } if ( Widget->GetParent() == nullptr || !WidgetsToExport.Contains(Widget->GetParent()) ) { auto SlotMetaData = NewObject(); SlotMetaData->SetWidget(Widget); UExporter::ExportToOutputDevice(&Context, SlotMetaData, nullptr, Archive, TEXT("copy"), 0, PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited, false, nullptr); } if (WidgetBlueprint) { for (const TSharedPtr& ClipboardExtension : ClipboardExtensions) { if (ClipboardExtension->CanAppendToClipboard(Widget)) { IClipboardExtension::FExportArgs ExportArgs; ExportArgs.Context = &Context; ExportArgs.Exporter = nullptr; ExportArgs.FileType = TEXT("copy"); ExportArgs.Indent = 0; ExportArgs.PortFlags = PPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited; ExportArgs.bSelectedOnly = false; ExportArgs.ExportRootScope = nullptr; ExportArgs.Out = &Archive; ClipboardExtension->AppendToClipboard(Widget, ExportArgs); } } } } ExportedText = Archive; } TArray FWidgetBlueprintEditorUtils::PasteWidgets(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, FWidgetReference ParentWidgetRef, FName SlotName, FVector2D PasteLocation) { FScopedTransaction Transaction(FGenericCommands::Get().Paste->GetDescription()); // Grab the text to paste from the clipboard. FString TextToImport; FPlatformApplicationMisc::ClipboardPaste(TextToImport); bool bTransactionSuccessful = true; TArray PastedWidgets = PasteWidgetsInternal(BlueprintEditor, BP, TextToImport, ParentWidgetRef, SlotName, PasteLocation, false, bTransactionSuccessful); if (!bTransactionSuccessful) { BlueprintEditor->LogSimpleMessage(LOCTEXT("PasteWidgetsCancel", "Paste operation on widget cancelled.")); Transaction.Cancel(); } return PastedWidgets; } bool FWidgetBlueprintEditorUtils::DisplayPasteWarningAndEarlyExit() { const FText DeleteConfirmationPrompt = LOCTEXT("DeleteConfirmationPrompt", "Pasting in a single-slot widget will erase its content. Do you wish to proceed?"); const FText DeleteConfirmationTitle = LOCTEXT("DeleteConfirmationTitle", "Delete widget"); // Warn the user that this may result in data loss FSuppressableWarningDialog::FSetupInfo Info(DeleteConfirmationPrompt, DeleteConfirmationTitle, TEXT("Paste_Warning")); Info.ConfirmText = LOCTEXT("DeleteConfirmation_Yes", "Yes"); Info.CancelText = LOCTEXT("DeleteConfirmation_No", "No"); FSuppressableWarningDialog DeleteChildWidgetWarningDialog(Info); return DeleteChildWidgetWarningDialog.ShowModal() == FSuppressableWarningDialog::Cancel; } TArray FWidgetBlueprintEditorUtils::PasteWidgetsInternal(TSharedRef BlueprintEditor, UWidgetBlueprint* BP, const FString& TextToImport, FWidgetReference ParentWidgetRef, FName SlotName, FVector2D PasteLocation, bool bForceSibling, bool& bTransactionSuccessful) { // Do an intial text processing to make sure we have any widgets to paste UPackage* TempPackage = nullptr; FWidgetObjectTextFactory Factory = ProcessImportedText(BP, TextToImport, TempPackage); TGCObjectScopeGuard TempPackageGCGuard(TempPackage); const bool bHasPastedWidget = Factory.NewWidgetMap.Num() > 0; // Ignore an empty set of widget paste data. if (!bHasPastedWidget) { bTransactionSuccessful = false; return TArray(); } TArray RootPasteWidgets; TMap PastedExtraSlotData; TSet PastedWidgets; auto ImportWidgets = [&]() { FWidgetBlueprintEditorUtils::ImportWidgetsFromText(BP, TextToImport, /*out*/ PastedWidgets, /*out*/ PastedExtraSlotData); for (UWidget* NewWidget : PastedWidgets) { BP->OnVariableAdded(NewWidget->GetFName()); // Widgets with a null parent mean that they were the root most widget of their selection set when // they were copied and thus we need to paste only the root most widgets. All their children will be added // automatically. if (NewWidget->GetParent() == nullptr) { // Check to see if this widget is content of another widget holding it in a named slot. bool bIsNamedSlot = false; for (UWidget* ContainerWidget : PastedWidgets) { if (INamedSlotInterface* NamedSlotContainer = Cast(ContainerWidget)) { if (NamedSlotContainer->ContainsContent(NewWidget)) { bIsNamedSlot = true; break; } } } // It's a Root widget only if it's not not in a named slot. if (!bIsNamedSlot) { RootPasteWidgets.Add(NewWidget); } } } }; // If we're pasting into a content widget of the same type, treat it as a sibling duplication UWidget* FirstPastedWidget = Factory.NewWidgetMap.CreateIterator()->Value; if (FirstPastedWidget->IsA(UContentWidget::StaticClass()) && ParentWidgetRef.IsValid() && FirstPastedWidget->GetClass() == ParentWidgetRef.GetTemplate()->GetClass()) { UPanelWidget* TargetParentWidget = ParentWidgetRef.GetTemplate()->GetParent(); if (TargetParentWidget && TargetParentWidget->CanAddMoreChildren()) { bForceSibling = true; } } if ( SlotName == NAME_None ) { UPanelWidget* ParentWidget = nullptr; int32 IndexToInsert = INDEX_NONE; if ( ParentWidgetRef.IsValid() ) { ParentWidget = Cast(ParentWidgetRef.GetTemplate()); // If the widget isn't a panel or we just really want it to be a sibling (ie. when duplicating), we'll try it's parent to see if the pasted widget can be a sibling (and get its index to insert at) if ( bForceSibling || !ParentWidget ) { if (UWidget* WidgetTemplate = ParentWidgetRef.GetTemplate()) { ParentWidget = WidgetTemplate->GetParent(); if (ParentWidget && ParentWidget->CanHaveMultipleChildren()) { IndexToInsert = ParentWidget->GetChildIndex(WidgetTemplate) + 1; } } } } if ( !ParentWidget ) { // If we already have a root widget, then we can't replace the root. if ( BP->WidgetTree->RootWidget ) { bTransactionSuccessful = false; return TArray(); } } if ( ParentWidget ) { // If parent widget can only have one child and that slot is already occupied, we will remove its contents so the pasted widgets can be inserted in their place UWidget* ChildWidgetToDelete = nullptr; if (!ParentWidget->CanHaveMultipleChildren() && ParentWidget->GetChildrenCount() > 0) { // We do not Remove child if there is nothing to paste. if ( bHasPastedWidget ) { if (FWidgetBlueprintEditorUtils::DisplayPasteWarningAndEarlyExit()) { bTransactionSuccessful = false; return TArray(); } // Delete the singular child ChildWidgetToDelete = ParentWidget->GetAllChildren()[0]; ChildWidgetToDelete->SetFlags(RF_Transactional); ChildWidgetToDelete->Modify(); ParentWidget->SetFlags(RF_Transactional); ParentWidget->Modify(); ParentWidget->RemoveChild(ChildWidgetToDelete); } } if (ChildWidgetToDelete) { DeleteWidgets(BP, { ChildWidgetToDelete }, EDeleteWidgetWarningType::DeleteSilently); } } ImportWidgets(); // If there isn't a root widget and we're copying multiple root widgets, then we need to add a container root // to hold the pasted data since multiple root widgets isn't permitted. if (!ParentWidget && RootPasteWidgets.Num() > 1) { ParentWidget = BP->WidgetTree->ConstructWidget(UCanvasPanel::StaticClass()); BP->WidgetTree->Modify(); BP->WidgetTree->RootWidget = ParentWidget; BP->OnVariableAdded(ParentWidget->GetFName()); } if (ParentWidget) { // A bit of a hack, but we can look at the widget's slot properties to determine if it is a canvas slot. If so, we'll try and maintain the relative positions bool bShouldReproduceOffsets = true; static const FName LayoutDataLabel = FName(TEXT("LayoutData")); for (TPair SlotData : PastedExtraSlotData) { UWidgetSlotPair* SlotDataPair = SlotData.Value; TMap SlotProperties; SlotDataPair->GetSlotProperties(SlotProperties); if (!SlotProperties.Contains(LayoutDataLabel)) { bShouldReproduceOffsets = false; break; } } FVector2D FirstWidgetPosition; ParentWidget->Modify(); for ( UWidget* NewWidget : RootPasteWidgets ) { UPanelSlot* Slot; if ( IndexToInsert == INDEX_NONE ) { Slot = ParentWidget->AddChild(NewWidget); } else { Slot = ParentWidget->InsertChildAt(IndexToInsert, NewWidget); } if ( Slot ) { if ( UWidgetSlotPair* OldSlotData = PastedExtraSlotData.FindRef(NewWidget->GetFName()) ) { TMap OldSlotProperties; OldSlotData->GetSlotProperties(OldSlotProperties); FWidgetBlueprintEditorUtils::ImportPropertiesFromText(Slot, OldSlotProperties); // Cache the initial position of the first widget so we can calculate offsets for additional widgets if (NewWidget == RootPasteWidgets[0]) { if (UCanvasPanelSlot* FirstCanvasSlot = Cast(Slot)) { FirstWidgetPosition = FirstCanvasSlot->GetPosition(); } } } BlueprintEditor->RefreshPreview(); FWidgetReference WidgetRef = BlueprintEditor->GetReferenceFromTemplate(NewWidget); UPanelSlot* PreviewSlot = WidgetRef.GetPreview()->Slot; UPanelSlot* TemplateSlot = WidgetRef.GetTemplate()->Slot; if ( UCanvasPanelSlot* CanvasSlot = Cast(PreviewSlot) ) { FVector2D PasteOffset = FVector2D(0, 0); if (bShouldReproduceOffsets) { PasteOffset = CanvasSlot->GetPosition()- FirstWidgetPosition; } if (UCanvasPanel* Canvas = Cast(CanvasSlot->Parent)) { Canvas->TakeWidget(); // Generate the underlying widget so redoing the layout below works. } CanvasSlot->SaveBaseLayout(); CanvasSlot->SetDesiredPosition(PasteLocation + PasteOffset); CanvasSlot->RebaseLayout(); } TMap SlotProperties; FWidgetBlueprintEditorUtils::ExportPropertiesToText(PreviewSlot, SlotProperties); FWidgetBlueprintEditorUtils::ImportPropertiesFromText(TemplateSlot, SlotProperties); } } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); } else { check(RootPasteWidgets.Num() == 1) // If we've arrived here, we must be creating the root widget from paste data, and there can only be // one item in the paste data by now. BP->WidgetTree->Modify(); if (RootPasteWidgets.Num() > 0) { BP->WidgetTree->RootWidget = RootPasteWidgets[0]; } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); } } else { ImportWidgets(); if ( RootPasteWidgets.Num() > 1 ) { FNotificationInfo Info(LOCTEXT("NamedSlotsOnlyHoldOneWidget", "Can't paste content, a slot can only hold one widget at the root.")); FSlateNotificationManager::Get().AddNotification(Info); bTransactionSuccessful = false; return TArray(); } BP->WidgetTree->Modify(); // If there's a ParentWidgetRef, then we're pasting into a named slot of a widget in the tree. if (UWidget* NamedSlotHostWidget = ParentWidgetRef.GetTemplate()) { NamedSlotHostWidget->SetFlags(RF_Transactional); NamedSlotHostWidget->Modify(); INamedSlotInterface* NamedSlotInterface = Cast(NamedSlotHostWidget); NamedSlotInterface->SetContentForSlot(SlotName, RootPasteWidgets[0]); } else { // If there's no ParentWidgetRef then we're pasting into the exposed named slots of the widget tree. // these are the slots that our parent class is exposing for use externally, but we can also override // them as a subclass. BP->WidgetTree->Modify(); BP->WidgetTree->SetContentForSlot(SlotName, RootPasteWidgets[0]); } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); } return RootPasteWidgets; } FWidgetObjectTextFactory FWidgetBlueprintEditorUtils::ProcessImportedText(UWidgetBlueprint* BP, const FString& TextToImport, /*out*/ UPackage*& TempPackage) { // We create our own transient package here so that we can deserialize the data in isolation and ensure unreferenced // objects not part of the deserialization set are unresolved. TempPackage = NewObject(nullptr, TEXT("/Engine/UMG/Editor/Transient"), RF_Transient); // Force the transient package to have the same namespace as the final widget blueprint package. // This ensures any text properties serialized from the buffer will be keyed correctly for the target package. #if USE_STABLE_LOCALIZATION_KEYS { const FString PackageNamespace = TextNamespaceUtil::EnsurePackageNamespace(BP); if (!PackageNamespace.IsEmpty()) { TextNamespaceUtil::ForcePackageNamespace(TempPackage, PackageNamespace); } } #endif // USE_STABLE_LOCALIZATION_KEYS // Turn the text buffer into objects FWidgetObjectTextFactory Factory; Factory.ProcessBuffer(TempPackage, RF_Transactional, TextToImport); return Factory; } void FWidgetBlueprintEditorUtils::ImportWidgetsFromText(UWidgetBlueprint* BP, const FString& TextToImport, /*out*/ TSet& ImportedWidgetSet, /*out*/ TMap& PastedExtraSlotData) { UPackage* TempPackage = nullptr; FWidgetObjectTextFactory Factory = ProcessImportedText(BP, TextToImport, TempPackage); TGCObjectScopeGuard TempPackageGCGuard(TempPackage); IUMGEditorModule& EditorModule = FModuleManager::LoadModuleChecked("UMGEditor"); const TArrayView> ClipboardExtensions = EditorModule.GetClipboardExtensibilityManager()->GetExtensions(); for (const TSharedPtr& ClipboardExtension : ClipboardExtensions) { ClipboardExtension->ProcessImportedText(BP, TextToImport, TempPackage); } PastedExtraSlotData = Factory.MissingSlotData; for ( auto& Entry : Factory.NewWidgetMap ) { UWidget* Widget = Entry.Value; ImportedWidgetSet.Add(Widget); Widget->SetFlags(RF_Transactional); // We don't export parent slot pointers, so each panel will need to point it's children back to itself UPanelWidget* PanelWidget = Cast(Widget); if (PanelWidget) { TArray PanelSlots = PanelWidget->GetSlots(); for (int32 i = 0; i < PanelWidget->GetChildrenCount(); i++) { UWidget* PanelChild = PanelWidget->GetChildAt(i); if (ensure(PanelChild)) { PanelChild->Slot = PanelSlots[i]; } } } // If there is an existing widget with the same name, rename the newly placed widget. FString WidgetOldName = Widget->GetName(); FString NewName = FindNextValidName(BP->WidgetTree, WidgetOldName); if (NewName != WidgetOldName) { UWidgetSlotPair* SlotData = PastedExtraSlotData.FindRef(Widget->GetFName()); if ( SlotData ) { PastedExtraSlotData.Remove(Widget->GetFName()); } Widget->Rename(*NewName, BP->WidgetTree); if (Widget->GetDisplayLabel().Equals(WidgetOldName)) { Widget->SetDisplayLabel(Widget->GetName()); } if ( SlotData ) { SlotData->SetWidgetName(Widget->GetFName()); PastedExtraSlotData.Add(Widget->GetFName(), SlotData); } } else { Widget->Rename(*WidgetOldName, BP->WidgetTree); } for (const TSharedPtr& Extension : ClipboardExtensions) { if (Extension->CanImportFromClipboard(Widget)) { Extension->ImportDataToWidget(Widget, FName(WidgetOldName)); } } } } void FWidgetBlueprintEditorUtils::ExportPropertiesToText(UObject* Object, TMap& ExportedProperties) { if ( Object ) { static const TSet SpecialCaseProperties = { TEXT("bIsVariable") }; for ( TFieldIterator PropertyIt(Object->GetClass()); PropertyIt && PropertyIt.GetStruct() != UVisual::StaticClass(); ++PropertyIt ) { FProperty* Property = *PropertyIt; // Skip EditDefaultOnly, transient, and instanced properties, only include properties that the user can directly edit and some special cases if (!Property->HasAnyPropertyFlags(CPF_TextExportTransient | CPF_Transient | CPF_DuplicateTransient | CPF_DisableEditOnInstance) && ( Property->HasAllPropertyFlags(CPF_Edit) || Property->IsA() || SpecialCaseProperties.Contains(Property->GetFName()))) { FString ValueText; if (Property->ExportText_InContainer(0, ValueText, Object, Object, Object, PPF_Copy)) { ExportedProperties.Add(Property->GetFName(), ValueText); } } } } } void FWidgetBlueprintEditorUtils::ImportPropertiesFromText(UObject* Object, const TMap& ExportedProperties) { if ( Object ) { for ( const auto& Entry : ExportedProperties ) { if ( FProperty* Property = FindFProperty(Object->GetClass(), Entry.Key) ) { FEditPropertyChain PropertyChain; PropertyChain.AddHead(Property); Object->PreEditChange(PropertyChain); Property->ImportText_InContainer(*Entry.Value, Object, Object, PPF_Copy); FPropertyChangedEvent ChangedEvent(Property); Object->PostEditChangeProperty(ChangedEvent); } } } } bool FWidgetBlueprintEditorUtils::DoesClipboardTextContainWidget(UWidgetBlueprint* BP) { FString TextToImport; FPlatformApplicationMisc::ClipboardPaste(TextToImport); UPackage* TempPackage = nullptr; FWidgetObjectTextFactory Factory = ProcessImportedText(BP, TextToImport, TempPackage); return Factory.NewWidgetMap.Num() > 0; } bool FWidgetBlueprintEditorUtils::IsBindWidgetProperty(const FProperty* InProperty) { bool bIsOptional; return IsBindWidgetProperty(InProperty, bIsOptional); } bool FWidgetBlueprintEditorUtils::IsBindWidgetProperty(const FProperty* InProperty, bool& bIsOptional) { if ( InProperty ) { bool bIsBindWidget = InProperty->HasMetaData("BindWidget") || InProperty->HasMetaData("BindWidgetOptional"); bIsOptional = InProperty->HasMetaData("BindWidgetOptional") || ( InProperty->HasMetaData("OptionalWidget") || InProperty->GetBoolMetaData("OptionalWidget") ); return bIsBindWidget; } return false; } bool FWidgetBlueprintEditorUtils::IsBindWidgetAnimProperty(const FProperty* InProperty) { bool bIsOptional; return IsBindWidgetAnimProperty(InProperty, bIsOptional); } bool FWidgetBlueprintEditorUtils::IsBindWidgetAnimProperty(const FProperty* InProperty, bool& bIsOptional) { if (InProperty) { bool bIsBindWidgetAnim = InProperty->HasMetaData("BindWidgetAnim") || InProperty->HasMetaData("BindWidgetAnimOptional"); bIsOptional = InProperty->HasMetaData("BindWidgetAnimOptional"); return bIsBindWidgetAnim; } return false; } namespace UE::UMG::Private { /** Helper class to perform path based filtering for unloaded BP's */ class FUnloadedBlueprintData : public IUnloadedBlueprintData { public: FUnloadedBlueprintData(const FAssetData& InAssetData) :ClassPath() , ClassFlags(CLASS_None) , bIsNormalBlueprintType(false) { ClassName = MakeShared(InAssetData.AssetName.ToString()); FString GeneratedClassPath; const UClass* AssetClass = InAssetData.GetClass(); if (AssetClass && AssetClass->IsChildOf(UBlueprintGeneratedClass::StaticClass())) { ClassPath = InAssetData.ToSoftObjectPath().GetAssetPathString(); } else if (InAssetData.GetTagValue(FBlueprintTags::GeneratedClassPath, GeneratedClassPath)) { ClassPath = FTopLevelAssetPath(*FPackageName::ExportTextPathToObjectPath(GeneratedClassPath)); } FEditorClassUtils::GetImplementedInterfaceClassPathsFromAsset(InAssetData, ImplementedInterfaces); } virtual ~FUnloadedBlueprintData() { } // Begin IUnloadedBlueprintData interface virtual bool HasAnyClassFlags(uint32 InFlagsToCheck) const { return (ClassFlags & InFlagsToCheck) != 0; } virtual bool HasAllClassFlags(uint32 InFlagsToCheck) const { return ((ClassFlags & InFlagsToCheck) == InFlagsToCheck); } virtual void SetClassFlags(uint32 InFlags) { ClassFlags = InFlags; } virtual bool ImplementsInterface(const UClass* InInterface) const { FString InterfacePath = InInterface->GetPathName(); for (const FString& ImplementedInterface : ImplementedInterfaces) { if (ImplementedInterface == InterfacePath) { return true; } } return false; } virtual bool IsChildOf(const UClass* InClass) const { return false; } virtual bool IsA(const UClass* InClass) const { // Unloaded blueprint classes should always be a BPGC, so this just checks against the expected type. return UBlueprintGeneratedClass::StaticClass()->UObject::IsA(InClass); } virtual const UClass* GetClassWithin() const { return nullptr; } virtual const UClass* GetNativeParent() const { return nullptr; } virtual void SetNormalBlueprintType(bool bInNormalBPType) { bIsNormalBlueprintType = bInNormalBPType; } virtual bool IsNormalBlueprintType() const { return bIsNormalBlueprintType; } virtual TSharedPtr GetClassName() const { return ClassName; } virtual FName GetClassPath() const { PRAGMA_DISABLE_DEPRECATION_WARNINGS return ClassPath.ToFName(); PRAGMA_ENABLE_DEPRECATION_WARNINGS } virtual FTopLevelAssetPath GetClassPathName() const { return ClassPath; } // End IUnloadedBlueprintData interface private: TSharedPtr ClassName; FTopLevelAssetPath ClassPath; uint32 ClassFlags; TArray ImplementedInterfaces; bool bIsNormalBlueprintType; }; bool IsUsableWidgetClass(const FString& WidgetPathName, const FAssetData& WidgetAssetData, FName Category, const UClass* WidgetClass, TSharedRef InCurrentActiveBlueprintEditor) { const UWidgetEditingProjectSettings* UMGEditorProjectSettings = FWidgetBlueprintEditorUtils::GetRelevantSettings(InCurrentActiveBlueprintEditor); // Excludes engine content if user sets it to false if (!UMGEditorProjectSettings->bShowWidgetsFromEngineContent) { if (WidgetPathName.StartsWith(TEXT("/Engine"))) { return false; } } // Excludes developer content if user sets it to false if (!UMGEditorProjectSettings->bShowWidgetsFromDeveloperContent) { if (WidgetPathName.StartsWith(TEXT("/Game/Developers"))) { return false; } } UWidgetBlueprint* WidgetBP = InCurrentActiveBlueprintEditor->GetWidgetBlueprintObj(); const bool bAllowEditorWidget = WidgetBP ? WidgetBP->AllowEditorWidget() : false; if (!bAllowEditorWidget) { if (WidgetClass && IsEditorOnlyObject(WidgetClass)) { return false; } else if (WidgetAssetData.IsValid()) { // should not load since the default for GetClass is EResolveClass::No const UClass* AssetClass = WidgetAssetData.GetClass(); if (AssetClass && IsEditorOnlyObject(AssetClass)) { return false; } } } if (UMGEditorProjectSettings->bUseEditorConfigPaletteFiltering) { FClassViewerModule* ClassViewerModule = FModuleManager::GetModulePtr("ClassViewer"); const TSharedPtr GlobalClassFilter = ClassViewerModule ? ClassViewerModule->GetGlobalClassViewerFilter() : TSharedPtr(); if (UMGEditorProjectSettings->GetAllowedPaletteCategories().PassesFilter(Category) && GlobalClassFilter.IsValid()) { if (WidgetClass) { return GlobalClassFilter->IsClassAllowed(FClassViewerInitializationOptions(), WidgetClass, ClassViewerModule->CreateFilterFuncs()); } else if (WidgetAssetData.IsValid()) { TSharedRef UnloadedBlueprint = MakeShared(WidgetAssetData); return GlobalClassFilter->IsUnloadedClassAllowed(FClassViewerInitializationOptions(), UnloadedBlueprint, ClassViewerModule->CreateFilterFuncs()); } } auto IsPathUnderMountPoints = [](FStringView Path) { static const FString EnginePath = TEXT("Engine"); static const FString GamePath = TEXT("Game"); const TSet& MountPoints = IPluginManager::Get().GetBuiltInPluginNames(); if (MountPoints.Num() > 0) { const FStringView MountPoint = FPathViews::GetMountPointNameFromPath(Path); return MountPoints.ContainsByHash(GetTypeHash(MountPoint), MountPoint) || MountPoint.Equals(EnginePath, ESearchCase::IgnoreCase) || MountPoint.Equals(GamePath, ESearchCase::IgnoreCase); } return false; }; const bool bPassesAllowedPalletteFilter = UMGEditorProjectSettings->GetAllowedPaletteWidgets().PassesFilter(WidgetPathName); if (FPackageName::IsScriptPackage(WidgetPathName)) { return bPassesAllowedPalletteFilter; } const bool bPathUnderMountPoints = IsPathUnderMountPoints(WidgetPathName); if (bPathUnderMountPoints && !bPassesAllowedPalletteFilter) { return false; } return true; } else { // Excludes this widget if it is on the hide list for (const FSoftClassPath& WidgetClassToHide : UMGEditorProjectSettings->WidgetClassesToHide) { if (WidgetPathName.Find(WidgetClassToHide.ToString()) == 0) { return false; } } } return true; } } bool FWidgetBlueprintEditorUtils::IsUsableWidgetClass(const UClass* WidgetClass) { return false; } TValueOrError FWidgetBlueprintEditorUtils::IsUsableWidgetClass(const FAssetData& WidgetAsset) { return MakeError(); } bool FWidgetBlueprintEditorUtils::IsUsableWidgetClass(const UClass* WidgetClass, TSharedRef BlueprintEditor) { if (WidgetClass->IsChildOf(UWidget::StaticClass())) { // We aren't interested in classes that are experimental or cannot be instantiated bool bIsExperimental, bIsEarlyAccess; FString MostDerivedDevelopmentClassName; FObjectEditorUtils::GetClassDevelopmentStatus(const_cast(WidgetClass), bIsExperimental, bIsEarlyAccess, MostDerivedDevelopmentClassName); const bool bIsInvalid = WidgetClass->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists); if (bIsExperimental || bIsEarlyAccess || bIsInvalid) { return false; } // Don't include skeleton classes or the same class as the widget being edited const bool bIsSkeletonClass = WidgetClass->HasAnyFlags(RF_Transient) && WidgetClass->HasAnyClassFlags(CLASS_CompiledFromBlueprint); // Check that the asset that generated this class is valid (necessary b/c of a larger issue wherein force delete does not wipe the generated class object) if (bIsSkeletonClass) { return false; } return UE::UMG::Private::IsUsableWidgetClass(WidgetClass->GetPathName(), FAssetData(), *WidgetClass->GetDefaultObject()->GetPaletteCategory().ToString(), WidgetClass, BlueprintEditor); } return false; } TValueOrError FWidgetBlueprintEditorUtils::IsUsableWidgetClass(const FAssetData& WidgetAsset, TSharedRef InCurrentActiveBlueprintEditor) { if (const UClass* WidgetAssetClass = WidgetAsset.GetClass(EResolveClass::No)) { if (IsUsableWidgetClass(WidgetAssetClass, InCurrentActiveBlueprintEditor)) { FWidgetBlueprintEditorUtils::FUsableWidgetClassResult Result; Result.NativeParentClass = WidgetAssetClass; Result.AssetClassFlags = WidgetAssetClass->GetClassFlags(); return MakeValue(Result); } } // Blueprints get the class type actions for their parent native class - this avoids us having to load the blueprint UClass* NativeParentClass = nullptr; FString NativeParentClassName; WidgetAsset.GetTagValue(FBlueprintTags::NativeParentClassPath, NativeParentClassName); if (NativeParentClassName.IsEmpty()) { return MakeError(); } else { const FString NativeParentClassPath = FPackageName::ExportTextPathToObjectPath(NativeParentClassName); if (NativeParentClassPath.StartsWith(TEXT("/"))) { // Metadata may be pointing to classes that no longer exist, so check for redirectors first const FString RedirectedClassPath = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Class, FCoreRedirectObjectName(NativeParentClassPath)).ToString(); NativeParentClass = UClass::TryFindTypeSlow(RedirectedClassPath); } if (NativeParentClass == nullptr) { return MakeError(); } if (!NativeParentClass->IsChildOf(UWidget::StaticClass())) { return MakeError(); } } EClassFlags BPFlags = static_cast(WidgetAsset.GetTagValueRef(FBlueprintTags::ClassFlags)); const bool bIsInvalid = (BPFlags & (CLASS_Deprecated | CLASS_Abstract | CLASS_NewerVersionExists)) != 0; if (bIsInvalid) { return MakeError(); } const FName CategoryName = *GetPaletteCategory(WidgetAsset, TSubclassOf(NativeParentClass)).ToString(); if (UE::UMG::Private::IsUsableWidgetClass(WidgetAsset.GetObjectPathString(), WidgetAsset, CategoryName, nullptr, InCurrentActiveBlueprintEditor)) { FWidgetBlueprintEditorUtils::FUsableWidgetClassResult Result; Result.NativeParentClass = NativeParentClass; Result.AssetClassFlags = BPFlags; return MakeValue(Result); } return MakeError(); } FString RemoveSuffixFromName(const FString OldName) { int NameLen = OldName.Len(); int SuffixIndex = 0; if (OldName.FindLastChar('_', SuffixIndex)) { NameLen = SuffixIndex; for (int32 i = SuffixIndex + 1; i < OldName.Len(); ++i) { const TCHAR& C = OldName[i]; const bool bGoodChar = ((C >= '0') && (C <= '9')); if (!bGoodChar) { return OldName; } } } return FString::ConstructFromPtrSize(*OldName, NameLen); } FString FWidgetBlueprintEditorUtils::FindNextValidName(UWidgetTree* WidgetTree, const FString& Name) { // If the name of the widget is not already used, we use it. if (FindObject(WidgetTree, *Name)) { // If the name is already used, we will suffix it with '_X' FString NameWithoutSuffix = RemoveSuffixFromName(Name); FString NewName = NameWithoutSuffix; int32 Postfix = 0; while (FindObject(WidgetTree, *NewName)) { ++Postfix; NewName = FString::Printf(TEXT("%s_%d"), *NameWithoutSuffix, Postfix); } return NewName; } return Name; } UWidgetTree* FWidgetBlueprintEditorUtils::FindLatestWidgetTree(UWidgetBlueprint* Blueprint, UUserWidget* UserWidget) { UWidgetTree* LatestWidgetTree = Blueprint->WidgetTree; // If there is no RootWidget, we look for a WidgetTree in the parents classes until we find one. if (LatestWidgetTree->RootWidget == nullptr) { UWidgetBlueprintGeneratedClass* BGClass = UserWidget->GetWidgetTreeOwningClass(); // If we find a class that owns the widget tree, just make sure it's not our current class, that would imply we've removed all the widgets // from this current tree, and if we use this classes compiled tree it's going to be the outdated old version. if (BGClass && BGClass != Blueprint->GeneratedClass) { LatestWidgetTree = BGClass->GetWidgetTreeArchetype(); } } return LatestWidgetTree; } int32 FWidgetBlueprintEditorUtils::UpdateHittestGrid(FHittestGrid& HitTestGrid, TSharedRef Window, float Scale, FVector2D DrawSize, float DeltaTime) { FSlateApplication::Get().InvalidateAllWidgets(false); const FGeometry WindowGeometry = FGeometry::MakeRoot(DrawSize * (1.f / Scale), FSlateLayoutTransform(Scale)); const FSlateRect WindowClipRect = WindowGeometry.GetLayoutBoundingRect(); FPaintArgs PaintArgs(nullptr, HitTestGrid, FVector2D::ZeroVector, FApp::GetCurrentTime(), DeltaTime); FSlateRenderer* MainSlateRenderer = FSlateApplication::Get().GetRenderer(); FScopeLock ScopeLock(MainSlateRenderer->GetResourceCriticalSection()); Window->SlatePrepass(WindowGeometry.Scale); PaintArgs.GetHittestGrid().SetHittestArea(WindowClipRect.GetTopLeft(), WindowClipRect.GetSize()); PaintArgs.GetHittestGrid().Clear(); // Get the free buffer & add our virtual window bool bUseGammaSpace = false; TSharedPtr Renderer = FModuleManager::Get().LoadModuleChecked("SlateRHIRenderer") .CreateSlate3DRenderer(bUseGammaSpace); int32 MaxLayerId = 0; { ISlate3DRenderer::FScopedAcquireDrawBuffer ScopedDrawBuffer{ *Renderer }; FSlateWindowElementList& WindowElementList = ScopedDrawBuffer.GetDrawBuffer().AddWindowElementList(Window); MaxLayerId = Window->Paint( PaintArgs, WindowGeometry, WindowClipRect, WindowElementList, 0, FWidgetStyle(), Window->IsEnabled()); } FSlateApplication::Get().InvalidateAllWidgets(false); return MaxLayerId; } TTuple FWidgetBlueprintEditorUtils::GetWidgetPreviewAreaAndSize(UUserWidget* UserWidget, FVector2D DesiredSize, FVector2D PreviewSize, EDesignPreviewSizeMode SizeMode, TOptional ThumbnailCustomSize) { FVector2D Size(PreviewSize.X, PreviewSize.Y); FVector2D Area(PreviewSize.X, PreviewSize.Y); if (UserWidget) { switch (SizeMode) { case EDesignPreviewSizeMode::Custom: Area = ThumbnailCustomSize.IsSet()? ThumbnailCustomSize.GetValue() : UserWidget->DesignTimeSize; // If the custom size is 0 in some dimension, use the desired size instead. if (Area.X == 0) { Area.X = DesiredSize.X; } if (Area.Y == 0) { Area.Y = DesiredSize.Y; } Size = Area; break; case EDesignPreviewSizeMode::CustomOnScreen: Size = ThumbnailCustomSize.IsSet() ? ThumbnailCustomSize.GetValue() : UserWidget->DesignTimeSize; // If the custom size is 0 in some dimension, use the desired size instead. if (Size.X == 0) { Size.X = DesiredSize.X; } if (Size.Y == 0) { Size.Y = DesiredSize.Y; } return TTuple(Area, Size); case EDesignPreviewSizeMode::Desired: Area = DesiredSize; // Fall through to DesiredOnScreen case EDesignPreviewSizeMode::DesiredOnScreen: Size = DesiredSize; return TTuple(Area, Size); case EDesignPreviewSizeMode::FillScreen: break; } } return TTuple(Area, Size); } float FWidgetBlueprintEditorUtils::GetWidgetPreviewDPIScale(UUserWidget* UserWidget, FVector2D PreviewSize) { // If the user is using a custom size then we disable the DPI scaling logic. if (UserWidget) { if (UserWidget->DesignSizeMode == EDesignPreviewSizeMode::Custom || UserWidget->DesignSizeMode == EDesignPreviewSizeMode::Desired) { return 1.0f; } } return GetDefault()->GetDPIScaleBasedOnSize(FIntPoint(FMath::TruncToInt32(PreviewSize.X), FMath::TruncToInt32(PreviewSize.Y))); } FVector2D FWidgetBlueprintEditorUtils::GetWidgetPreviewUnScaledCustomSize(FVector2D DesiredSize, UUserWidget* UserWidget, TOptional ThumbnailCustomSize, EThumbnailPreviewSizeMode ThumbnailSizeMode) { checkf(DesiredSize.X > 0.f && DesiredSize.Y > 0.f, TEXT("The size should have been previously checked to be > 0.")); FVector2D FinalSize(0.f,0.f); int32 PreviewWidth; const TCHAR* ConfigSectionName = TEXT("UMGEditor.Designer"); GConfig->GetInt(ConfigSectionName, TEXT("PreviewWidth"), PreviewWidth, GEditorPerProjectIni); int32 PreviewHeight; GConfig->GetInt(ConfigSectionName, TEXT("PreviewHeight"), PreviewHeight, GEditorPerProjectIni); FVector2D PreviewSize(PreviewWidth, PreviewHeight); TTuple AreaAndSize = GetWidgetPreviewAreaAndSize(UserWidget, DesiredSize, PreviewSize, ConvertThumbnailSizeModeToDesignerSizeMode(ThumbnailSizeMode, UserWidget), ThumbnailCustomSize.IsSet() ? ThumbnailCustomSize.GetValue() : TOptional()); float DPIScale; if (ThumbnailCustomSize.IsSet()) { DPIScale = 1.0f; } else { DPIScale = GetWidgetPreviewDPIScale(UserWidget, PreviewSize); } if (ensure(DPIScale > 0.f)) { FinalSize = AreaAndSize.Get<1>() / DPIScale; } return FinalSize; } EDesignPreviewSizeMode FWidgetBlueprintEditorUtils::ConvertThumbnailSizeModeToDesignerSizeMode(EThumbnailPreviewSizeMode ThumbnailSizeMode, UUserWidget* WidgetInstance) { switch (ThumbnailSizeMode) { case EThumbnailPreviewSizeMode::MatchDesignerMode: return WidgetInstance->DesignSizeMode; case EThumbnailPreviewSizeMode::FillScreen: return EDesignPreviewSizeMode::FillScreen; case EThumbnailPreviewSizeMode::Custom: return EDesignPreviewSizeMode::Custom; case EThumbnailPreviewSizeMode::Desired: return EDesignPreviewSizeMode::Desired; default: return EDesignPreviewSizeMode::Desired; } } TOptional FWidgetBlueprintEditorUtils::DrawSWidgetInRenderTarget(UUserWidget* WidgetInstance, UTextureRenderTarget2D* RenderTarget2D) { return DrawSWidgetInRenderTargetInternal(WidgetInstance, nullptr, RenderTarget2D, FVector2D(256.f,256.f), false, TOptional(), EThumbnailPreviewSizeMode::MatchDesignerMode); } UWidgetEditingProjectSettings* FWidgetBlueprintEditorUtils::GetRelevantMutableSettings(TWeakPtr CurrentEditor) { if (TSharedPtr PinnedEditor = CurrentEditor.Pin()) { if (UWidgetBlueprint* WidgetBP = PinnedEditor->GetWidgetBlueprintObj()) { return WidgetBP->GetRelevantSettings(); } } // Fall back to the UMG Editor settings as default return GetMutableDefault(); } const UWidgetEditingProjectSettings* FWidgetBlueprintEditorUtils::GetRelevantSettings(TWeakPtr CurrentEditor) { if (TSharedPtr PinnedEditor = CurrentEditor.Pin()) { if (UWidgetBlueprint* WidgetBP = PinnedEditor->GetWidgetBlueprintObj()) { return WidgetBP->GetRelevantSettings(); } } // Fall back to the UMG Editor settings as default return GetDefault(); } TOptional FWidgetBlueprintEditorUtils::DrawSWidgetInRenderTargetForThumbnail(UUserWidget* WidgetInstance, FRenderTarget* RenderTarget2D, FVector2D ThumbnailSize, TOptional ThumbnailCustomSize, EThumbnailPreviewSizeMode ThumbnailSizeMode) { return DrawSWidgetInRenderTargetInternal(WidgetInstance, RenderTarget2D, nullptr,ThumbnailSize, true, ThumbnailCustomSize, ThumbnailSizeMode); } TOptional FWidgetBlueprintEditorUtils::DrawSWidgetInRenderTargetForThumbnail(UUserWidget* WidgetInstance, UTextureRenderTarget2D* RenderTarget2D, FVector2D ThumbnailSize, TOptional ThumbnailCustomSize, EThumbnailPreviewSizeMode ThumbnailSizeMode) { return DrawSWidgetInRenderTargetInternal(WidgetInstance, nullptr, RenderTarget2D,ThumbnailSize, true, ThumbnailCustomSize, ThumbnailSizeMode); } TOptional FWidgetBlueprintEditorUtils::DrawSWidgetInRenderTargetInternal(UUserWidget* WidgetInstance, FRenderTarget* RenderTarget2D, UTextureRenderTarget2D* TextureRenderTarget,FVector2D ThumbnailSize, bool bIsForThumbnail, TOptional ThumbnailCustomSize, EThumbnailPreviewSizeMode ThumbnailSizeMode) { if (TextureRenderTarget == nullptr && RenderTarget2D == nullptr) { return TOptional(); } //Create Window FVector2D Offset(0.f, 0.f); FVector2D ScaledSize(0.f, 0.f); TSharedPtr WindowContent = WidgetInstance->TakeWidget(); if (!WindowContent) { return TOptional(); } TSharedRef Window = SNew(SVirtualWindow); TUniquePtr HitTestGrid = MakeUnique(); Window->SetContent(WindowContent.ToSharedRef()); Window->Resize(ThumbnailSize); // Store the desired size to maintain the aspect ratio later FGeometry WindowGeometry = FGeometry::MakeRoot(ThumbnailSize, FSlateLayoutTransform(1.0f)); Window->SlatePrepass(1.0f); FVector2D DesiredSizeWindow = Window->GetDesiredSize(); if (DesiredSizeWindow.X < SMALL_NUMBER || DesiredSizeWindow.Y < SMALL_NUMBER) { return TOptional(); } FVector2D UnscaledSize = FWidgetBlueprintEditorUtils::GetWidgetPreviewUnScaledCustomSize(DesiredSizeWindow, WidgetInstance, ThumbnailCustomSize, ThumbnailSizeMode); if (UnscaledSize.X < SMALL_NUMBER || UnscaledSize.Y < SMALL_NUMBER) { return TOptional(); } float Scale = 1.f; // Change some configuration if it is for thumbnail creation if (bIsForThumbnail) { TTuple ScaleAndOffset = FWidgetBlueprintEditorUtils::GetThumbnailImageScaleAndOffset(UnscaledSize, ThumbnailSize); Scale = ScaleAndOffset.Get<0>(); Offset = ScaleAndOffset.Get<1>(); } ScaledSize = UnscaledSize * Scale; if (ScaledSize.X < 1.f || ScaledSize.Y < 1.f) { return TOptional(); } // Create Renderer Target and WidgetRenderer bool bApplyGammaCorrection = RenderTarget2D? true : false; FWidgetRenderer* WidgetRenderer = new FWidgetRenderer(bApplyGammaCorrection); if (!bIsForThumbnail) { WidgetRenderer->SetIsPrepassNeeded(false); } if (TextureRenderTarget) { TextureRenderTarget->Filter = TF_Bilinear; TextureRenderTarget->ClearColor = FLinearColor::Transparent; TextureRenderTarget->SRGB = true; TextureRenderTarget->RenderTargetFormat = RTF_RGBA8; uint32 ScaledSizeX = static_cast(ScaledSize.X); uint32 ScaledSizeY = static_cast(ScaledSize.Y); const bool bForceLinearGamma = false; const EPixelFormat RequestedFormat = FSlateApplication::Get().GetRenderer()->GetSlateRecommendedColorFormat(); TextureRenderTarget->InitCustomFormat(ScaledSizeX, ScaledSizeY, RequestedFormat, bForceLinearGamma); WidgetRenderer->DrawWindow(TextureRenderTarget, *HitTestGrid, Window, Scale, ScaledSize, 0.1f); } else { ensure(RenderTarget2D != nullptr); WidgetRenderer->SetShouldClearTarget(false); WidgetRenderer->ViewOffset = Offset; WidgetRenderer->DrawWindow(RenderTarget2D, *HitTestGrid, Window, Scale, ScaledSize, 0.1f); } if (WidgetRenderer) { BeginCleanup(WidgetRenderer); WidgetRenderer = nullptr; } return TOptional(FWidgetBlueprintEditorUtils::FWidgetThumbnailProperties{ ScaledSize,Offset }); } TTuple FWidgetBlueprintEditorUtils::GetThumbnailImageScaleAndOffset(FVector2D WidgetSize, FVector2D ThumbnailSize) { // Scale the widget blueprint image to fit in the thumbnail checkf(WidgetSize.X > 0.f && WidgetSize.Y > 0.f, TEXT("The size should have been previously checked to be > 0.")); float Scale; double XOffset = 0; double YOffset = 0; if (WidgetSize.X > WidgetSize.Y) { Scale = static_cast(ThumbnailSize.X / WidgetSize.X); WidgetSize *= Scale; YOffset = (ThumbnailSize.Y - WidgetSize.Y) / 2.f; } else { Scale = static_cast(ThumbnailSize.Y / WidgetSize.Y); WidgetSize *= Scale; XOffset = (ThumbnailSize.X - WidgetSize.X) / 2.f; } return TTuple(Scale, FVector2D(XOffset, YOffset)); } void FWidgetBlueprintEditorUtils::SetTextureAsAssetThumbnail(UWidgetBlueprint* WidgetBlueprint, UTexture2D* ThumbnailTexture) { const TCHAR* ThumbnailName = TEXT("Thumbnail"); UTexture2D* ExistingThumbnail = FindObject(WidgetBlueprint, ThumbnailName, false); if (ExistingThumbnail) { ExistingThumbnail->Rename(nullptr, GetTransientPackage()); } if (!ThumbnailTexture) { WidgetBlueprint->ThumbnailImage = nullptr; return; } FVector2D TextureSize(ThumbnailTexture->GetSizeX(), ThumbnailTexture->GetSizeY()); if (TextureSize.X < SMALL_NUMBER || TextureSize.Y < SMALL_NUMBER) { WidgetBlueprint->ThumbnailImage = nullptr; } else { ThumbnailTexture->Rename(ThumbnailName, WidgetBlueprint, REN_NonTransactional | REN_DontCreateRedirectors); WidgetBlueprint->ThumbnailImage = ThumbnailTexture; } } FText FWidgetBlueprintEditorUtils::GetPaletteCategory(const TSubclassOf WidgetClass) { if (WidgetClass.Get()) { return WidgetClass.GetDefaultObject()->GetPaletteCategory(); } return GetMutableDefault()->GetPaletteCategory(); } FText FWidgetBlueprintEditorUtils::GetPaletteCategory(const FAssetData& WidgetAsset, const TSubclassOf NativeClass) { //The asset can be a UBlueprint, UBlueprintGeneratedClass, a UWidgetBlueprint or a UWidgetBlueprintGeneratedClass if (UClass* WidgetAssetClass = WidgetAsset.GetClass(EResolveClass::No)) { if (WidgetAssetClass->IsChildOf(UWidget::StaticClass())) { return GetPaletteCategory(TSubclassOf(WidgetAssetClass)); } } //If the blueprint is unloaded we need to extract it from the asset metadata. FText FoundPaletteCategoryText = WidgetAsset.GetTagValueRef(GET_MEMBER_NAME_CHECKED(UWidgetBlueprint, PaletteCategory)); if (!FoundPaletteCategoryText.IsEmpty()) { return FoundPaletteCategoryText; } else if (NativeClass.Get() != nullptr && NativeClass->IsChildOf(UWidget::StaticClass()) && !NativeClass->IsChildOf(UUserWidget::StaticClass())) { return NativeClass.GetDefaultObject()->GetPaletteCategory(); } static const FTopLevelAssetPath BlueprintGeneratedClassAssetPath = UWidgetBlueprintGeneratedClass::StaticClass()->GetClassPathName(); static const FTopLevelAssetPath WidgetBlueprintAssetPath = UWidgetBlueprint::StaticClass()->GetClassPathName(); if (WidgetAsset.AssetClassPath == BlueprintGeneratedClassAssetPath || WidgetAsset.AssetClassPath == WidgetBlueprintAssetPath) { return GetMutableDefault()->GetPaletteCategory(); } else { return GetMutableDefault()->GetPaletteCategory(); } } UWidgetBlueprint* FWidgetBlueprintEditorUtils::GetWidgetBlueprintFromWidget(const UWidget* Widget) { if (Widget) { if (UObject* WidgetTree = Widget->GetOuter()) { UWidgetBlueprint* WidgetBlueprint = Cast(WidgetTree->GetOuter()); if (WidgetBlueprint) { return WidgetBlueprint; } else if (WidgetTree->GetOuter()) { WidgetBlueprint = Cast(WidgetTree->GetOuter()->GetClass()->ClassGeneratedBy); if (WidgetBlueprint) { return WidgetBlueprint; } } } } return nullptr; } TSet FWidgetBlueprintEditorUtils::ResolveWidgetTemplates(const TSet& Widgets) { TSet Templates; Algo::TransformIf(Widgets, Templates, [&](const FWidgetReference& Widget) { return Widget.GetTemplate() != nullptr; }, [&](const FWidgetReference& Widget) { return Widget.GetTemplate(); }); return Templates; } #undef LOCTEXT_NAMESPACE