// Copyright Epic Games, Inc. All Rights Reserved. #include "SSocketManager.h" #include "Widgets/Layout/SSplitter.h" #include "UObject/UnrealType.h" #include "Engine/StaticMesh.h" #include "Modules/ModuleManager.h" #include "UObject/UObjectHash.h" #include "UObject/UObjectIterator.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Input/SButton.h" #include "Widgets/Views/SListView.h" #include "EditorStyleSet.h" #include "Components/StaticMeshComponent.h" #include "Editor/UnrealEdEngine.h" #include "Engine/StaticMeshSocket.h" #include "UnrealEdGlobals.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Editor/StaticMeshEditor/Public/IStaticMeshEditor.h" #include "Editor/PropertyEditor/Public/PropertyEditorModule.h" #include "ScopedTransaction.h" #include "Runtime/Analytics/Analytics/Public/Interfaces/IAnalyticsProvider.h" #include "EngineAnalytics.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "Framework/Commands/GenericCommands.h" #define LOCTEXT_NAMESPACE "SSCSSocketManagerEditor" struct SocketListItem { public: SocketListItem(UStaticMeshSocket* InSocket) : Socket(InSocket) { } /** The static mesh socket this represents */ UStaticMeshSocket* Socket; /** Delegate for when the context menu requests a rename */ DECLARE_DELEGATE(FOnRenameRequested); FOnRenameRequested OnRenameRequested; }; class SSocketDisplayItem: public STableRow> { public: SLATE_BEGIN_ARGS(SSocketDisplayItem) {} /** The socket this item displays. */ SLATE_ARGUMENT(TWeakPtr, SocketItem) /** Pointer back to the socket manager */ SLATE_ARGUMENT(TWeakPtr, SocketManagerPtr) SLATE_END_ARGS() /** * Construct the widget * * @param InArgs A declaration from which to construct the widget */ void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { SocketItem = InArgs._SocketItem; SocketManagerPtr = InArgs._SocketManagerPtr; TSharedPtr InlineWidget; this->ChildSlot .Padding(0.0f, 3.0f, 6.0f, 3.0f) .VAlign(VAlign_Center) [SAssignNew(InlineWidget, SInlineEditableTextBlock) .Text(this, &SSocketDisplayItem::GetSocketName) .OnVerifyTextChanged(this, &SSocketDisplayItem::OnVerifySocketNameChanged) .OnTextCommitted(this, &SSocketDisplayItem::OnCommitSocketName) .IsSelected(this, &STableRow>::IsSelectedExclusively)]; TSharedPtr SocketItemPinned = SocketItem.Pin(); if (SocketItemPinned.IsValid()) { SocketItemPinned->OnRenameRequested.BindSP(InlineWidget.Get(), &SInlineEditableTextBlock::EnterEditingMode); } STableRow>::ConstructInternal( STableRow::FArguments() .ShowSelection(true), InOwnerTableView); } private: /** Returns the socket name */ FText GetSocketName() const { TSharedPtr SocketItemPinned = SocketItem.Pin(); return SocketItemPinned.IsValid() ? FText::FromName(SocketItemPinned->Socket->SocketName) : FText(); } bool OnVerifySocketNameChanged(const FText& InNewText, FText& OutErrorMessage) { bool bVerifyName = true; FText NewText = FText::TrimPrecedingAndTrailing(InNewText); if (NewText.IsEmpty()) { OutErrorMessage = LOCTEXT("EmptySocketName_Error", "Sockets must have a name!"); bVerifyName = false; } else { TSharedPtr SocketItemPinned = SocketItem.Pin(); TSharedPtr SocketManagerPinned = SocketManagerPtr.Pin(); if (SocketItemPinned.IsValid() && SocketItemPinned->Socket != nullptr && SocketItemPinned->Socket->SocketName.ToString() != NewText.ToString() && SocketManagerPinned.IsValid() && SocketManagerPinned->CheckForDuplicateSocket(NewText.ToString())) { OutErrorMessage = LOCTEXT("DuplicateSocket_Error", "Socket name in use!"); bVerifyName = false; } } return bVerifyName; } void OnCommitSocketName(const FText& InText, ETextCommit::Type CommitInfo) { FText NewText = FText::TrimPrecedingAndTrailing(InText); TSharedPtr PinnedSocketItem = SocketItem.Pin(); if (PinnedSocketItem.IsValid()) { UStaticMeshSocket* SelectedSocket = PinnedSocketItem->Socket; if (SelectedSocket != NULL) { FScopedTransaction Transaction(LOCTEXT("SetSocketName", "Set Socket Name")); FProperty* ChangedProperty = FindFProperty(UStaticMeshSocket::StaticClass(), "SocketName"); // Pre edit, calls modify on the object SelectedSocket->PreEditChange(ChangedProperty); // Edit the property itself SelectedSocket->SocketName = FName(*NewText.ToString()); // Post edit FPropertyChangedEvent PropertyChangedEvent(ChangedProperty); SelectedSocket->PostEditChangeProperty(PropertyChangedEvent); } } } private: /** The Socket to display. */ TWeakPtr SocketItem; /** Pointer back to the socket manager */ TWeakPtr SocketManagerPtr; }; TSharedPtr ISocketManager::CreateSocketManager(TSharedPtr InStaticMeshEditor, FSimpleDelegate InOnSocketSelectionChanged) { TSharedPtr SocketManager; SAssignNew(SocketManager, SSocketManager) .StaticMeshEditorPtr(InStaticMeshEditor) .OnSocketSelectionChanged(InOnSocketSelectionChanged); TSharedPtr ISocket; ISocket = StaticCastSharedPtr(SocketManager); return ISocket; } void SSocketManager::Construct(const FArguments& InArgs) { StaticMeshEditorPtr = InArgs._StaticMeshEditorPtr; OnSocketSelectionChanged = InArgs._OnSocketSelectionChanged; TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (!StaticMeshEditorPinned.IsValid()) { return; } // Register a post undo function which keeps the socket manager list view consistent with the static mesh StaticMeshEditorPinned->RegisterOnPostUndo(IStaticMeshEditor::FOnPostUndo::CreateSP(this, &SSocketManager::PostUndo)); StaticMesh = StaticMeshEditorPinned->GetStaticMesh(); FDetailsViewArgs Args; Args.bHideSelectionTip = true; Args.bLockable = false; Args.bAllowSearch = false; Args.bShowOptions = false; Args.NotifyHook = this; Args.NameAreaSettings = FDetailsViewArgs::HideNameArea; FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); SocketDetailsView = PropertyModule.CreateDetailView(Args); WorldSpaceRotation = FVector::ZeroVector; this->ChildSlot [SNew(SVerticalBox) + SVerticalBox::Slot() .FillHeight(1.0f) [SNew(SSplitter) .Orientation(Orient_Horizontal) + SSplitter::Slot() .Value(.3f) [SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(0, 0, 0, 4) [SNew(SButton) .ButtonStyle(FEditorStyle::Get(), "FlatButton.Success") .ForegroundColor(FLinearColor::White) .Text(LOCTEXT("CreateSocket", "Create Socket")) .OnClicked(this, &SSocketManager::CreateSocket_Execute) .HAlign(HAlign_Center)] + SVerticalBox::Slot() .FillHeight(1.0f) [SAssignNew(SocketListView, SListView>) .SelectionMode(ESelectionMode::Single) .ListItemsSource(&SocketList) // Generates the actual widget for a tree item .OnGenerateRow(this, &SSocketManager::MakeWidgetFromOption) // Find out when the user selects something in the tree .OnSelectionChanged(this, &SSocketManager::SocketSelectionChanged_Execute) // Allow for some spacing between items with a larger item height. .ItemHeight(20.0f) .OnContextMenuOpening(this, &SSocketManager::OnContextMenuOpening) .OnItemScrolledIntoView(this, &SSocketManager::OnItemScrolledIntoView) .HeaderRow( SNew(SHeaderRow) .Visibility(EVisibility::Collapsed) + SHeaderRow::Column(TEXT("Socket")))] + SVerticalBox::Slot() .AutoHeight() [SNew(SSeparator)] + SVerticalBox::Slot() .AutoHeight() [SNew(STextBlock) .Text(this, &SSocketManager::GetSocketHeaderText)]]] + SSplitter::Slot() .Value(.7f) [SNew(SOverlay) + SOverlay::Slot() [SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .Visibility(this, &SSocketManager::GetSelectSocketMessageVisibility) [SNew(STextBlock) .Text(LOCTEXT("NoSocketSelected", "Select a Socket"))]] + SOverlay::Slot() [SocketDetailsView.ToSharedRef()]]]]; RefreshSocketList(); AddPropertyChangeListenerToSockets(); } SSocketManager::~SSocketManager() { TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (StaticMeshEditorPinned.IsValid()) { StaticMeshEditorPinned->UnregisterOnPostUndo(this); } RemovePropertyChangeListenerFromSockets(); } UStaticMeshSocket* SSocketManager::GetSelectedSocket() const { if (SocketListView->GetSelectedItems().Num()) { return SocketListView->GetSelectedItems()[0]->Socket; } return nullptr; } EVisibility SSocketManager::GetSelectSocketMessageVisibility() const { return SocketListView->GetSelectedItems().Num() > 0 ? EVisibility::Hidden : EVisibility::Visible; } void SSocketManager::SetSelectedSocket(UStaticMeshSocket* InSelectedSocket) { if (InSelectedSocket) { for (int32 i = 0; i < SocketList.Num(); i++) { if (SocketList[i]->Socket == InSelectedSocket) { SocketListView->SetSelection(SocketList[i]); SocketListView->RequestListRefresh(); SocketSelectionChanged(InSelectedSocket); break; } } } else { SocketListView->ClearSelection(); SocketListView->RequestListRefresh(); SocketSelectionChanged(NULL); } } TSharedRef SSocketManager::MakeWidgetFromOption(TSharedPtr InItem, const TSharedRef& OwnerTable) { return SNew(SSocketDisplayItem, OwnerTable) .SocketItem(InItem) .SocketManagerPtr(SharedThis(this)); } void SSocketManager::CreateSocket() { TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (StaticMeshEditorPinned.IsValid()) { UStaticMesh* CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); const FScopedTransaction Transaction(LOCTEXT("CreateSocket", "Create Socket")); UStaticMeshSocket* NewSocket = NewObject(CurrentStaticMesh); check(NewSocket); if (FEngineAnalytics::IsAvailable()) { FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.StaticMesh.CreateSocket")); } FString SocketNameString = TEXT("Socket"); FName SocketName = FName(*SocketNameString); // Make sure the new name is valid int32 Index = 0; while (CheckForDuplicateSocket(SocketName.ToString())) { SocketName = FName(*FString::Printf(TEXT("%s%i"), *SocketNameString, Index)); ++Index; } NewSocket->SocketName = SocketName; NewSocket->SetFlags(RF_Transactional); NewSocket->OnPropertyChanged().AddSP(this, &SSocketManager::OnSocketPropertyChanged); CurrentStaticMesh->PreEditChange(NULL); CurrentStaticMesh->AddSocket(NewSocket); CurrentStaticMesh->PostEditChange(); CurrentStaticMesh->MarkPackageDirty(); TSharedPtr SocketItem = MakeShareable(new SocketListItem(NewSocket)); SocketList.Add(SocketItem); SocketListView->RequestListRefresh(); SocketListView->SetSelection(SocketItem); RequestRenameSelectedSocket(); } } void SSocketManager::DuplicateSelectedSocket() { TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); UStaticMeshSocket* SelectedSocket = GetSelectedSocket(); if (StaticMeshEditorPinned.IsValid() && SelectedSocket) { const FScopedTransaction Transaction(LOCTEXT("SocketManager_DuplicateSocket", "Duplicate Socket")); UStaticMesh* CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); UStaticMeshSocket* NewSocket = DuplicateObject(SelectedSocket, CurrentStaticMesh); // Create a unique name for this socket NewSocket->SocketName = MakeUniqueObjectName(CurrentStaticMesh, UStaticMeshSocket::StaticClass(), NewSocket->SocketName); // Add the new socket to the static mesh CurrentStaticMesh->PreEditChange(NULL); CurrentStaticMesh->AddSocket(NewSocket); CurrentStaticMesh->PostEditChange(); CurrentStaticMesh->MarkPackageDirty(); RefreshSocketList(); // Select the duplicated socket SetSelectedSocket(NewSocket); } } void SSocketManager::UpdateStaticMesh() { RefreshSocketList(); } void SSocketManager::RequestRenameSelectedSocket() { if (SocketListView->GetSelectedItems().Num() == 1) { TSharedPtr SocketItem = SocketListView->GetSelectedItems()[0]; SocketListView->RequestScrollIntoView(SocketItem); DeferredRenameRequest = SocketItem; } } void SSocketManager::DeleteSelectedSocket() { if (SocketListView->GetSelectedItems().Num()) { const FScopedTransaction Transaction(LOCTEXT("DeleteSocket", "Delete Socket")); TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (StaticMeshEditorPinned.IsValid()) { UStaticMesh* CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); CurrentStaticMesh->PreEditChange(NULL); UStaticMeshSocket* SelectedSocket = SocketListView->GetSelectedItems()[0]->Socket; SelectedSocket->OnPropertyChanged().RemoveAll(this); CurrentStaticMesh->Sockets.Remove(SelectedSocket); CurrentStaticMesh->PostEditChange(); RefreshSocketList(); } } } void SSocketManager::RefreshSocketList() { // The static mesh might not be the same one we built the SocketListView with // check it here and update it if necessary. TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (StaticMeshEditorPinned.IsValid()) { bool bIsSameStaticMesh = true; UStaticMesh* CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); if (!StaticMesh.IsValid() || CurrentStaticMesh != StaticMesh.Get()) { StaticMesh = CurrentStaticMesh; bIsSameStaticMesh = false; } // Only rebuild the socket list if it differs from the static meshes socket list // This is done so that an undo on a socket property doesn't cause the selected // socket to be de-selected, thus hiding the socket properties on the detail view. // NB: Also force a rebuild if the underlying StaticMesh has been changed. if (StaticMesh->Sockets.Num() != SocketList.Num() || !bIsSameStaticMesh) { SocketList.Empty(); for (int32 i = 0; i < StaticMesh->Sockets.Num(); i++) { UStaticMeshSocket* Socket = StaticMesh->Sockets[i]; SocketList.Add(MakeShareable(new SocketListItem(Socket))); } SocketListView->RequestListRefresh(); } // Set the socket on the detail view to keep it in sync with the sockets properties if (SocketListView->GetSelectedItems().Num()) { TArray ObjectList; ObjectList.Add(SocketListView->GetSelectedItems()[0]->Socket); SocketDetailsView->SetObjects(ObjectList, true); } StaticMeshEditorPinned->RefreshViewport(); } else { SocketList.Empty(); SocketListView->ClearSelection(); SocketListView->RequestListRefresh(); } } bool SSocketManager::CheckForDuplicateSocket(const FString& InSocketName) { for (int32 i = 0; i < SocketList.Num(); i++) { if (SocketList[i]->Socket->SocketName.ToString() == InSocketName) { return true; } } return false; } void SSocketManager::SocketSelectionChanged(UStaticMeshSocket* InSocket) { TArray SelectedObject; if (InSocket) { SelectedObject.Add(InSocket); } SocketDetailsView->SetObjects(SelectedObject); // Notify listeners OnSocketSelectionChanged.ExecuteIfBound(); } void SSocketManager::SocketSelectionChanged_Execute(TSharedPtr InItem, ESelectInfo::Type /*SelectInfo*/) { if (InItem.IsValid()) { SocketSelectionChanged(InItem->Socket); } else { SocketSelectionChanged(NULL); } } FReply SSocketManager::CreateSocket_Execute() { CreateSocket(); return FReply::Handled(); } FText SSocketManager::GetSocketHeaderText() const { UStaticMesh* CurrentStaticMesh = nullptr; TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (StaticMeshEditorPinned.IsValid()) { CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); } return FText::Format(LOCTEXT("SocketHeader_TotalFmt", "{0} sockets"), FText::AsNumber((CurrentStaticMesh != nullptr) ? CurrentStaticMesh->Sockets.Num() : 0)); } void SSocketManager::SocketName_TextChanged(const FText& InText) { CheckForDuplicateSocket(InText.ToString()); } TSharedPtr SSocketManager::OnContextMenuOpening() { const bool bShouldCloseWindowAfterMenuSelection = true; TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (!StaticMeshEditorPinned.IsValid()) { return TSharedPtr(); } FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, StaticMeshEditorPinned->GetToolkitCommands()); { MenuBuilder.BeginSection("BasicOperations"); { MenuBuilder.AddMenuEntry(FGenericCommands::Get().Delete); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Duplicate); MenuBuilder.AddMenuEntry(FGenericCommands::Get().Rename); } MenuBuilder.EndSection(); } return MenuBuilder.MakeWidget(); } void SSocketManager::NotifyPostChange(const FPropertyChangedEvent& PropertyChangedEvent, FProperty* PropertyThatChanged) { TArray> SelectedList = SocketListView->GetSelectedItems(); if (SelectedList.Num()) { if (PropertyThatChanged->GetName() == TEXT("Pitch") || PropertyThatChanged->GetName() == TEXT("Yaw") || PropertyThatChanged->GetName() == TEXT("Roll")) { const UStaticMeshSocket* Socket = SelectedList[0]->Socket; WorldSpaceRotation.Set(Socket->RelativeRotation.Pitch, Socket->RelativeRotation.Yaw, Socket->RelativeRotation.Roll); } } } void SSocketManager::AddPropertyChangeListenerToSockets() { TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (StaticMeshEditorPinned.IsValid()) { UStaticMesh* CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); for (int32 i = 0; i < CurrentStaticMesh->Sockets.Num(); ++i) { CurrentStaticMesh->Sockets[i]->OnPropertyChanged().AddSP(this, &SSocketManager::OnSocketPropertyChanged); } } } void SSocketManager::RemovePropertyChangeListenerFromSockets() { TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (StaticMeshEditorPinned.IsValid()) { UStaticMesh* CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); if (CurrentStaticMesh) { for (int32 i = 0; i < CurrentStaticMesh->Sockets.Num(); ++i) { CurrentStaticMesh->Sockets[i]->OnPropertyChanged().RemoveAll(this); } } } } void SSocketManager::OnSocketPropertyChanged(const UStaticMeshSocket* Socket, const FProperty* ChangedProperty) { static FName RelativeRotationName(TEXT("RelativeRotation")); static FName RelativeLocationName(TEXT("RelativeLocation")); check(Socket != nullptr); FName ChangedPropertyName = ChangedProperty->GetFName(); if (ChangedPropertyName == RelativeRotationName) { const UStaticMeshSocket* SelectedSocket = GetSelectedSocket(); if (Socket == SelectedSocket) { WorldSpaceRotation.Set(Socket->RelativeRotation.Pitch, Socket->RelativeRotation.Yaw, Socket->RelativeRotation.Roll); } } TSharedPtr StaticMeshEditorPinned = StaticMeshEditorPtr.Pin(); if (!StaticMeshEditorPinned.IsValid()) { return; } if (ChangedPropertyName == RelativeRotationName || ChangedPropertyName == RelativeLocationName) { // If socket location or rotation is changed, update the position of any actors attached to it in instances of this mesh UStaticMesh* CurrentStaticMesh = StaticMeshEditorPinned->GetStaticMesh(); if (CurrentStaticMesh != nullptr) { bool bUpdatedChild = false; for (TObjectIterator It; It; ++It) { if (It->GetStaticMesh() == CurrentStaticMesh) { const AActor* Actor = It->GetOwner(); if (Actor != nullptr) { const USceneComponent* Root = Actor->GetRootComponent(); if (Root != nullptr) { for (USceneComponent* Child: Root->GetAttachChildren()) { if (Child != nullptr && Child->GetAttachSocketName() == Socket->SocketName) { Child->UpdateComponentToWorld(); bUpdatedChild = true; } } } } } } if (bUpdatedChild) { GUnrealEd->RedrawLevelEditingViewports(); } } } } void SSocketManager::PostUndo() { RefreshSocketList(); } void SSocketManager::OnItemScrolledIntoView(TSharedPtr InItem, const TSharedPtr& InWidget) { TSharedPtr DeferredRenameRequestPinned = DeferredRenameRequest.Pin(); if (DeferredRenameRequestPinned.IsValid()) { DeferredRenameRequestPinned->OnRenameRequested.ExecuteIfBound(); DeferredRenameRequest.Reset(); } } #undef LOCTEXT_NAMESPACE