// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "IntroTutorialsPrivatePCH.h" #include "TutorialStructCustomization.h" #include "EditorTutorial.h" #include "STutorialEditableText.h" #include "ISlateMetaData.h" #include "TutorialMetaData.h" #include "SDockTab.h" #define LOCTEXT_NAMESPACE "TutorialStructCustomization" TSharedRef FTutorialContentCustomization::MakeInstance() { return MakeShareable( new FTutorialContentCustomization ); } void FTutorialContentCustomization::CustomizeHeader( TSharedRef InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { TSharedPtr TypeProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContent, Type)); TSharedPtr ContentProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContent, Content)); TSharedPtr ExcerptNameProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContent, ExcerptName)); TSharedPtr TextProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContent, Text)); // Show and hide various widgets based on current content type struct Local { static EVisibility GetContentVisibility(TSharedPtr InPropertyHandle) { check(InPropertyHandle.IsValid()); uint8 Value = 0; if(InPropertyHandle->GetValue(Value) == FPropertyAccess::Success) { const ETutorialContent::Type EnumValue = (ETutorialContent::Type)Value; return (EnumValue == ETutorialContent::UDNExcerpt) ? EVisibility::Visible : EVisibility::Collapsed; } return EVisibility::Collapsed; } static EVisibility GetExcerptNameVisibility(TSharedPtr InPropertyHandle) { check(InPropertyHandle.IsValid()); uint8 Value = 0; if(InPropertyHandle->GetValue(Value) == FPropertyAccess::Success) { const ETutorialContent::Type EnumValue = (ETutorialContent::Type)Value; return (EnumValue == ETutorialContent::UDNExcerpt) ? EVisibility::Visible : EVisibility::Collapsed; } return EVisibility::Collapsed; } static EVisibility GetTextVisibility(TSharedPtr InPropertyHandle) { check(InPropertyHandle.IsValid()); uint8 Value = 0; if(InPropertyHandle->GetValue(Value) == FPropertyAccess::Success) { const ETutorialContent::Type EnumValue = (ETutorialContent::Type)Value; return (EnumValue == ETutorialContent::Text) ? EVisibility::Visible : EVisibility::Collapsed; } return EVisibility::Collapsed; } static EVisibility GetRichTextVisibility(TSharedPtr InPropertyHandle) { check(InPropertyHandle.IsValid()); uint8 Value = 0; if(InPropertyHandle->GetValue(Value) == FPropertyAccess::Success) { const ETutorialContent::Type EnumValue = (ETutorialContent::Type)Value; return (EnumValue == ETutorialContent::RichText) ? EVisibility::Visible : EVisibility::Collapsed; } return EVisibility::Collapsed; } static FText GetValueAsText(TSharedPtr InPropertyHandle) { FText Text; if( InPropertyHandle->GetValueAsFormattedText( Text ) == FPropertyAccess::MultipleValues ) { Text = NSLOCTEXT("PropertyEditor", "MultipleValues", "Multiple Values"); } return Text; } static void OnTextCommitted( const FText& NewText, ETextCommit::Type /*CommitInfo*/, TSharedPtr InPropertyHandle ) { InPropertyHandle->SetValueFromFormattedString( NewText.ToString() ); } static void OnTextChanged( const FText& NewText, TSharedPtr InPropertyHandle ) { InPropertyHandle->SetValueFromFormattedString( NewText.ToString() ); } }; HeaderRow .NameContent() [ ContentProperty->CreatePropertyNameWidget() ] .ValueContent() .MinDesiredWidth(250.0f) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ TypeProperty->CreatePropertyValueWidget() ] +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ SNew(SHorizontalBox) .Visibility_Static(&Local::GetContentVisibility, TypeProperty) +SHorizontalBox::Slot() [ ContentProperty->CreatePropertyValueWidget() ] ] +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ SNew(SHorizontalBox) .Visibility_Static(&Local::GetExcerptNameVisibility, TypeProperty) +SHorizontalBox::Slot() [ ExcerptNameProperty->CreatePropertyValueWidget() ] ] +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ SNew(SHorizontalBox) .Visibility_Static(&Local::GetTextVisibility, TypeProperty) +SHorizontalBox::Slot() [ TextProperty->CreatePropertyValueWidget() ] ] +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ SNew(SHorizontalBox) .Visibility_Static(&Local::GetRichTextVisibility, TypeProperty) +SHorizontalBox::Slot() [ SNew(STutorialEditableText) .Text_Static(&Local::GetValueAsText, TextProperty) .OnTextCommitted(FOnTextCommitted::CreateStatic(&Local::OnTextCommitted, TextProperty)) .OnTextChanged(FOnTextChanged::CreateStatic(&Local::OnTextChanged, TextProperty)) ] ] ]; } void FTutorialContentCustomization::CustomizeChildren( TSharedRef InStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { } /** 'Tooltip' window to indicate what is currently being picked and how to abort the picker (Esc) */ class SWidgetPickerFloatingWindow : public SCompoundWidget { SLATE_BEGIN_ARGS(SWidgetPickerFloatingWindow){} SLATE_ARGUMENT(TWeakPtr, ParentWindow) SLATE_ARGUMENT(TSharedPtr, FriendlyNameProperty) SLATE_ARGUMENT(FName, SpecificWidgetType) SLATE_END_ARGS() void Construct(const FArguments& InArgs, TSharedRef InStructPropertyHandle, TSharedRef InPickPropertyHandle) { StructPropertyHandle = InStructPropertyHandle; PickPropertyHandle = InPickPropertyHandle; ParentWindow = InArgs._ParentWindow; SpecificWidgetType = InArgs._SpecificWidgetType; FriendlyNameProperty = InArgs._FriendlyNameProperty; ChildSlot [ SNew(SToolTip) .Text(this, &SWidgetPickerFloatingWindow::GetPickerStatusText) ]; } /* Returns the name of the picked widget */ FName GetPickedWidgetName() { return PickedWidgetName; } /** * Return the name of the given widget (Will filter out widgets that do no match the specific type if applicable * * @param InWidget The widget to get the pickable name of * @returns Pickable Name of the widget (or None if it doesnt match a specific widget type) * */ FName GetPickableNameForWidget(TSharedRef InWidget) const { FName PickableName; if (InWidget->GetTag() != NAME_None) { PickableName = InWidget->GetTag(); } else { // If we have specified a specific widget to to pick check this one matches TSharedPtr MetaData = InWidget->GetMetaData(); if (MetaData.IsValid()) { PickableName = MetaData->Tag; } else if (SpecificWidgetType != NAME_None) { FName TheType = InWidget->GetType(); if (TheType == SpecificWidgetType) { if (SpecificWidgetType == FName("SDockTab")) { TSharedRef DockTab = StaticCastSharedRef(InWidget); FString TabIdent = DockTab.Get().GetLayoutIdentifier().ToString(); PickableName = FName(*TabIdent); } } } } return PickableName; } private: virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override { PickedWidgetName = NAME_None; PickedAllMetaData.Reset(); FWidgetPath Path = FSlateApplication::Get().LocateWindowUnderMouse(FSlateApplication::Get().GetCursorPos(), FSlateApplication::Get().GetInteractiveTopLevelWindows(), true); for(int32 PathIndex = Path.Widgets.Num() - 1; PathIndex >= 0; PathIndex--) { TSharedRef PathWidget = Path.Widgets[PathIndex].Widget; PickedWidgetName = GetPickableNameForWidget(PathWidget); if (PickedWidgetName != NAME_None) { PickedAllMetaData = PathWidget->GetAllMetaData(); break; } } // kind of a hack, but we need to maintain keyboard focus otherwise we wont get our keypress to 'pick' FSlateApplication::Get().SetKeyboardFocus(SharedThis(this), EFocusCause::SetDirectly); if(ParentWindow.IsValid()) { // also kind of a hack, but this is the only way at the moment to get a 'cursor decorator' without using the drag-drop code path ParentWindow.Pin()->MoveWindowTo(FSlateApplication::Get().GetCursorPos() + FSlateApplication::Get().GetCursorSize()); } } FText GetPickerStatusText() const { return FText::Format(LOCTEXT("TootipHint", "{0} (Esc to pick)"), FText::FromName(PickedWidgetName)); } virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override { if(InKeyEvent.GetKey() == EKeys::Escape) { if( InKeyEvent.IsLeftControlDown() == false ) { // We cant set a parameter if this isn't valid ! check(PickPropertyHandle.IsValid()); PickPropertyHandle->SetValue(PickedWidgetName); TSharedPtr TypeProperty = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, Type)); TypeProperty->SetValue((uint8)ETutorialAnchorIdentifier::NamedWidget); FString FriendlyNameToSet = PickedWidgetName.ToString(); // Reset the other fields StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, GUIDString))->SetValue(FString()); StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, OuterName))->SetValue(FString()); StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, TabToFocusOrOpen))->SetValue(FString()); // Handle custom widget type picks if ((SpecificWidgetType.IsValid() == true) && (SpecificWidgetType != NAME_None)) { if (SpecificWidgetType == FName("SDockTab")) { StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, TabToFocusOrOpen))->SetValue(PickedWidgetName); } } for (const auto& MetaDataEntry : PickedAllMetaData) { if (MetaDataEntry->IsOfType()) { TSharedRef GraphNodeMeta = StaticCastSharedRef(MetaDataEntry); StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, GUIDString))->SetValue(GraphNodeMeta->GUID.ToString()); StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, OuterName))->SetValue(GraphNodeMeta->OuterName); StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, FriendlyName))->SetValue(GraphNodeMeta->FriendlyName); } else if (MetaDataEntry->IsOfType()) { TSharedRef TutorialMeta = StaticCastSharedRef(MetaDataEntry); FriendlyNameToSet = TutorialMeta->FriendlyName; // TabTypeToOpen only really applies to specifc widget types, so if we dont have one dont set the parameter if ((SpecificWidgetType.IsValid() == true) && (SpecificWidgetType != NAME_None)) { StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, TabToFocusOrOpen))->SetValue(TutorialMeta->TabTypeToOpen); } } else { TSharedRef GraphNodeMeta = StaticCastSharedRef(MetaDataEntry); FriendlyNameToSet = GraphNodeMeta->Tag.ToString(); } // Set the friendly name to the PickedWidget name - we might not have any metadata if (FriendlyNameProperty.IsValid()) { FriendlyNameProperty->SetValue(FriendlyNameToSet); } } } // Reset the pick data PickedWidgetName = NAME_None; PickedAllMetaData.Reset(); if(ParentWindow.IsValid()) { FSlateApplication::Get().RequestDestroyWindow(ParentWindow.Pin().ToSharedRef()); ParentWindow.Reset(); } return FReply::Handled(); } return FReply::Unhandled(); } /** We need to support keyboard focus to process the 'Esc' key */ virtual bool SupportsKeyboardFocus() const override { return true; } private: /** Handle to the property struct (Data other than the name is in here) */ TSharedPtr StructPropertyHandle; /** Handle to the name property struct. This is the property we are actually picking */ TSharedPtr PickPropertyHandle; /* Handle to the friendly name property we should set if any. */ TSharedPtr FriendlyNameProperty; /** Handle to the window that contains this widget */ TWeakPtr ParentWindow; /** The widget name we are picking */ FName PickedWidgetName; /* The metadata for the widget we are picking */ TArray> PickedAllMetaData; /* If we are we picking a specific widget type this will specify a typename (EG SDockTab) */ FName SpecificWidgetType; }; /** Widget used to launch a 'picking' session */ class SWidgetPicker : public SCompoundWidget { public: SLATE_BEGIN_ARGS( SWidgetPicker ) {} SLATE_ARGUMENT(FName, SpecificWidgetType) SLATE_ARGUMENT(TSharedPtr, FriendlyNameProperty) SLATE_END_ARGS() ~SWidgetPicker() { // kill the picker window as well if this widget is going away - that way we dont get dangling refs to the property if(PickerWindow.IsValid() && FSlateApplication::IsInitialized()) { FSlateApplication::Get().RequestDestroyWindow(PickerWindow.Pin().ToSharedRef()); PickerWindow.Reset(); PickerWidget.Reset(); } } void Construct(const FArguments& InArgs, TSharedRef InStructPropertyHandle, TSharedRef InPickPropertyHandle, IPropertyTypeCustomizationUtils* InStructCustomizationUtils) { StructPropertyHandle = InStructPropertyHandle; PickPropertyHandle = InPickPropertyHandle; SpecificWidgetType = InArgs._SpecificWidgetType; FriendlyNameProperty = InArgs._FriendlyNameProperty; ChildSlot [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(this, &SWidgetPicker::HandlePickerStatusText) .Font(InStructCustomizationUtils->GetRegularFont()) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SButton) .ButtonStyle( FEditorStyle::Get(), "HoverHintOnly" ) .OnClicked( this, &SWidgetPicker::OnClicked ) .ContentPadding(4.0f) .ForegroundColor( FSlateColor::UseForeground() ) .IsFocusable(false) [ SNew( SImage ) .Image( FEditorStyle::GetBrush("PropertyWindow.Button_PickActorInteractive") ) .ColorAndOpacity( FSlateColor::UseForeground() ) ] ] ]; } private: FReply OnClicked() { // launch a picker window if(!PickerWindow.IsValid()) { TSharedRef NewWindow = SWindow::MakeCursorDecorator(); NewWindow->MoveWindowTo(FSlateApplication::Get().GetCursorPos()); PickerWindow = NewWindow; NewWindow->SetContent( SAssignNew(PickerWidget, SWidgetPickerFloatingWindow, StructPropertyHandle.ToSharedRef(), PickPropertyHandle.ToSharedRef()) .ParentWindow(NewWindow) .SpecificWidgetType(SpecificWidgetType) .FriendlyNameProperty(FriendlyNameProperty) ); TSharedPtr RootWindow = FGlobalTabmanager::Get()->GetRootWindow(); if (RootWindow.IsValid()) { FSlateApplication::Get().AddWindowAsNativeChild(NewWindow, RootWindow.ToSharedRef()); } else { FSlateApplication::Get().AddWindow(NewWindow); } FIntroTutorials& IntroTutorials = FModuleManager::Get().GetModuleChecked("IntroTutorials"); IntroTutorials.OnIsPicking().BindSP(this, &SWidgetPicker::OnIsPicking); IntroTutorials.OnValidatePickingCandidate().BindSP(this, &SWidgetPicker::OnValidatePickingCandidate); } return FReply::Handled(); } FText HandlePickerStatusText() const { if (PickPropertyHandle.IsValid()) { FString WidgetValue; PickPropertyHandle->GetValue(WidgetValue); if (FriendlyNameProperty.IsValid()) { FString FriendlyName; FriendlyNameProperty->GetValue(FriendlyName); if (FriendlyName.IsEmpty() == false) { WidgetValue = FriendlyName; } } return FText::FromString(WidgetValue); } return FText(); } FText MakeFriendlyStringFromName(const FString& WidgetName) const { // We will likely have meta data for this eventually. For now just parse a comma delimited string which currently will either be a name or ident,name,UID if (WidgetName == "None") { return FText::FromName(*WidgetName); } FString FriendlyName; StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, FriendlyName))->GetValue(FriendlyName); if (FriendlyName.IsEmpty()==false) { return FText::FromName(*FriendlyName); } return FText::FromName(*WidgetName); } bool OnIsPicking(FName& OutWidgetNameToHighlight) const { if(PickerWidget.IsValid()) { OutWidgetNameToHighlight = PickerWidget.Pin()->GetPickedWidgetName(); return true; } return false; } bool OnValidatePickingCandidate(TSharedRef InWidget, FName& OutWidgetNameToHighlight, bool& bOutShouldHighlight) const { bool bIsPicking = false; bOutShouldHighlight = false; TSharedPtr MetaData = InWidget->GetMetaData(); const FName Tag = (MetaData.IsValid() && MetaData->Tag.IsValid()) ? MetaData->Tag : InWidget->GetTag(); if (PickerWidget.IsValid()) { // Is the given widget a candidate if ((Tag != NAME_None) ) { bIsPicking = true; } if (SpecificWidgetType != NAME_None) { bIsPicking = InWidget->GetType() == SpecificWidgetType; } // If we are picking a widget check if we should also highlight it if (bIsPicking == true) { OutWidgetNameToHighlight = PickerWidget.Pin()->GetPickedWidgetName(); FName InPickableName = PickerWidget.Pin()->GetPickableNameForWidget(InWidget); if (InPickableName == OutWidgetNameToHighlight) { bOutShouldHighlight = true; } } } return bIsPicking; } private: /** Picker window widget */ TWeakPtr PickerWidget; /** Picker window */ TWeakPtr PickerWindow; /** Handle to the struct we are customizing */ TSharedPtr StructPropertyHandle; /** Handle to the property we are customizing */ TSharedPtr PickPropertyHandle; /* Handle to the friendly name property we should set if any. */ TSharedPtr FriendlyNameProperty; /* Are we picking a specific widget type */ FName SpecificWidgetType; }; TSharedRef FTutorialContentAnchorCustomization::MakeInstance() { return MakeShareable( new FTutorialContentAnchorCustomization ); } void FTutorialContentAnchorCustomization::CustomizeHeader( TSharedRef InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { TSharedPtr DrawHighlightProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, bDrawHighlight)); TSharedPtr WidgetNameProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, WrapperIdentifier)); TSharedPtr TabToFocusProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, TabToFocusOrOpen)); TSharedPtr FriendlyNameProperty = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTutorialContentAnchor, FriendlyName)); HeaderRow .NameContent() [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ InStructPropertyHandle->CreatePropertyNameWidget() ] +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ DrawHighlightProperty->CreatePropertyNameWidget() ] + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ TabToFocusProperty->CreatePropertyNameWidget() ] ] .ValueContent() .MinDesiredWidth(250.0f) .MaxDesiredWidth(500.0f) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ SNew(SWidgetPicker, InStructPropertyHandle, WidgetNameProperty.ToSharedRef(), &StructCustomizationUtils) .FriendlyNameProperty(FriendlyNameProperty) ] + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ DrawHighlightProperty->CreatePropertyValueWidget() ] + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 2.0f) [ SNew(SWidgetPicker, InStructPropertyHandle, TabToFocusProperty.ToSharedRef(), &StructCustomizationUtils) .SpecificWidgetType("SDockTab") ] ]; } void FTutorialContentAnchorCustomization::CustomizeChildren( TSharedRef InStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) { } #undef LOCTEXT_NAMESPACE