// Copyright Steve Streeting 2022 // Released under the MIT license https://opensource.org/license/MIT/ #include "SUDSEditorToolkit.h" #include "EditorReimportHandler.h" #include "IDetailRootObjectCustomization.h" #include "IDocumentation.h" #include "SUDSDialogue.h" #include "SUDSEditorScriptTools.h" #include "SUDSEditorVoiceOverTools.h" #include "SUDSLibrary.h" #include "SUDSMessageLogger.h" #include "SUDSScript.h" #include "SUDSScriptNodeText.h" #include "SUDSSubsystem.h" #include "Framework/Text/SlateTextRun.h" #include "Misc/MessageDialog.h" #include "Widgets/Docking/SDockTab.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SMultiLineEditableTextBox.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/SToolTip.h" #include "Widgets/Views/SListView.h" #include "Toolkits/AssetEditorToolkit.h" #include "PropertyEditorModule.h" const FName NAME_SpeakerLine("SpeakerLine"); const FName NAME_Choice("Choice"); const FName NAME_VariableSet("VariableSet"); const FName NAME_VariableEdit("VariableEdit"); const FName NAME_SelectEval("Condition"); const FName NAME_RandomEval("Random"); const FName NAME_Event("Event"); const FName NAME_Start("Start"); const FName NAME_Finish("Finish"); FName FSUDSEditorToolkit::DialogueOutputTabName("SUDSDialogueTab"); FName FSUDSEditorToolkit::VariablesTabName("SUDSVariablesTab"); FName FSUDSEditorToolkit::DetailsTabName("SUDSDetailsTab"); FName FSUDSEditorToolkit::LogTabName("SUDSLogTab"); class FSUDSDetailRootObjectCustomization : public IDetailRootObjectCustomization { public: virtual TSharedPtr CustomizeObjectHeader(const FDetailsObjectSet& InRootObjectSet, const TSharedPtr& InTableRow) override { auto Ret = SNew(STextBlock) .Margin(FMargin(10,5)) .TextStyle(&FAppStyle::Get().GetWidgetStyle("SmallText")); if (auto TN = Cast(InRootObjectSet.RootObjects[0])) { Ret->SetText(FText::FromString(FString::Printf(TEXT("Speaker Line - ID: %s Source Line: %d"), *TN->GetTextID(), TN->GetSourceLineNo()))); } return Ret; } virtual bool ShouldDisplayHeader(const FDetailsObjectSet& InRootObjectSet) const override { return InRootObjectSet.RootObjects[0]->IsA(USUDSScriptNodeText::StaticClass()); } }; void FSUDSEditorToolkit::InitEditor(const TArray& InObjects) { if (InObjects.Num() > 0) { Script = Cast(InObjects[0]); ReimportDelegateHandle = FReimportManager::Instance()->OnPostReimport().AddRaw(this, &FSUDSEditorToolkit::OnPostReimport); const TSharedRef Layout = FTabManager::NewLayout("SUDSEditorLayout_v3") ->AddArea ( FTabManager::NewPrimaryArea()->SetOrientation(Orient_Horizontal) ->Split ( FTabManager::NewSplitter() ->SetSizeCoefficient(0.75f) ->SetOrientation(Orient_Horizontal) ->Split ( FTabManager::NewSplitter() ->SetSizeCoefficient(0.6f) ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.8f) ->AddTab("SUDSDialogueTab", ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.4f) ->AddTab("SUDSLogTab", ETabState::OpenedTab) ) ) ->Split ( FTabManager::NewSplitter() ->SetSizeCoefficient(0.25f) ->SetOrientation(Orient_Vertical) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.6f) ->AddTab("SUDSVariablesTab", ETabState::OpenedTab) ) ->Split ( FTabManager::NewStack() ->SetSizeCoefficient(0.4f) ->AddTab("SUDSDetailsTab", ETabState::OpenedTab) ) ) ) ); FAssetEditorToolkit::InitAssetEditor(EToolkitMode::Standalone, {}, "SUDSEditor", Layout, true, true, InObjects); } } void FSUDSEditorToolkit::RegisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::RegisterTabSpawners(InTabManager); WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(INVTEXT("SUDS Editor")); InTabManager->RegisterTabSpawner(DialogueOutputTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) { OutputListView = SNew(SListView>) #if ENGINE_MINOR_VERSION < 5 .ItemHeight(24) #endif .SelectionMode(ESelectionMode::None) .ListItemsSource(&OutputRows) .OnGenerateRow(this, &FSUDSEditorToolkit::OnGenerateRowForOutput) .HeaderRow( SNew(SHeaderRow) + SHeaderRow::Column("PrefixHeader") .FillSized(PrefixColumnWidth) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew( STextBlock ) .Text( INVTEXT("") ) ] ] + SHeaderRow::Column("LineHeader") .FillWidth(1.0f) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew( STextBlock ) .Text( INVTEXT("") ) ] ] ); ChoicesBox = SNew(SVerticalBox); return SNew(SDockTab) [ SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(1) [ OutputListView.ToSharedRef() ] +SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Left) .Padding(30, 15, 30, 15) [ ChoicesBox.ToSharedRef() ] ]; })) .SetDisplayName(INVTEXT("Dialogue Output")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()); InTabManager->RegisterTabSpawner(VariablesTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) { VariablesListView = SNew(SListView>) #if ENGINE_MINOR_VERSION < 5 .ItemHeight(28) #endif .SelectionMode(ESelectionMode::None) .ListItemsSource(&VariableRows) .OnGenerateRow(this, &FSUDSEditorToolkit::OnGenerateRowForVariable) .HeaderRow( SNew(SHeaderRow) + SHeaderRow::Column("NameHeader") .FillSized(VarColumnWidth) .VAlignCell(VAlign_Center) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(INVTEXT("Name")) ] ] + SHeaderRow::Column("ValueHeader") .FillWidth(1.0f) .VAlignCell(VAlign_Fill) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(INVTEXT("Value")) ] ] ); // To ensure globals are pre-populated UpdateVariables(); // Possibly use a SPropertyTable with a custom IPropertyTable to implement variable binding return SNew(SDockTab) [ SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Top) .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Right) .Padding(2) [ SNew(SButton) .Text(INVTEXT("Add Variable")) .OnClicked(this, &FSUDSEditorToolkit::AddVariableClicked) ] ] + SVerticalBox::Slot() .FillHeight(1.0) [ VariablesListView.ToSharedRef() ] ]; })) .SetDisplayName(INVTEXT("Variables")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()); InTabManager->RegisterTabSpawner(DetailsTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) { FPropertyEditorModule& PropertyEditorModule = FModuleManager::Get().LoadModuleChecked("PropertyEditor"); FDetailsViewArgs DetailsViewArgs; DetailsViewArgs.bShowOptions = false; DetailsViewArgs.bUpdatesFromSelection = false; DetailsViewArgs.bShowPropertyMatrixButton = false; DetailsViewArgs.bAllowSearch = false; DetailsViewArgs.bAllowMultipleTopLevelObjects = true; DetailsViewArgs.bShowObjectLabel = false; DetailsViewArgs.bHideSelectionTip = true; auto DetailView = PropertyEditorModule.CreateDetailView(DetailsViewArgs); DetailView->SetRootObjectCustomizationInstance(MakeShareable(new FSUDSDetailRootObjectCustomization())); TArray ObjectsInDetailView; ObjectsInDetailView.Add(Script); for (auto Node : Script->GetNodes()) { if (auto TNode = Cast(Node)) { ObjectsInDetailView.Add(TNode); } } DetailView->SetObjects(ObjectsInDetailView, true, true); // Possibly use a SPropertyTable with a custom IPropertyTable to implement variable binding return SNew(SDockTab) [ SNew(SVerticalBox) + SVerticalBox::Slot() .VAlign(VAlign_Top) [ DetailView ] ]; })) .SetDisplayName(INVTEXT("Details")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()); InTabManager->RegisterTabSpawner(LogTabName, FOnSpawnTab::CreateLambda([this](const FSpawnTabArgs&) { TraceLog = SNew(SSUDSTraceLog); return SNew(SDockTab) [ TraceLog.ToSharedRef() ]; })) .SetDisplayName(INVTEXT("Trace Log")) .SetGroup(WorkspaceMenuCategory.ToSharedRef()); // Set up the toolbar FSUDSToolbarCommands::Register(); const TWeakPtr WeakToolkit = this->AsShared(); TSharedRef ToolbarExtender = MakeShareable(new FExtender); ToolbarExtender->AddToolBarExtension( "Asset", EExtensionHook::After, GetToolkitCommands(), FToolBarExtensionDelegate::CreateRaw(this, &FSUDSEditorToolkit::ExtendToolbar, TWeakPtr(TabManager->GetOwnerTab()))); AddToolbarExtender(ToolbarExtender); GetToolkitCommands()->MapAction(FSUDSToolbarCommands::Get().StartDialogue, FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::StartDialogue), FCanExecuteAction()); GetToolkitCommands()->MapAction(FSUDSToolbarCommands::Get().WriteBackTextIDs, FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::WriteBackTextIDs), FCanExecuteAction()); GetToolkitCommands()->MapAction(FSUDSToolbarCommands::Get().GenerateVOAssets, FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::GenerateVOAssets), FCanExecuteAction()); //RegenerateMenusAndToolbars(); } void FSUDSEditorToolkit::EnsureTabsVisible() { // Hard requirement for the dialogue output tab to be open, so restore it if the user had // closed it (probably by accident) if (!TabManager->FindExistingLiveTab(DialogueOutputTabName)) { TabManager->TryInvokeTab(DialogueOutputTabName); } } void FSUDSEditorToolkit::ExtendToolbar(FToolBarBuilder& ToolbarBuilder, TWeakPtr Tab) { if (!Tab.IsValid()) { return; } ToolbarBuilder.BeginSection("CachedState"); { ToolbarBuilder.AddToolBarButton(FSUDSToolbarCommands::Get().StartDialogue, NAME_None, TAttribute(), TAttribute(), FSlateIcon( #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 FAppStyle::GetAppStyleSetName(), #else FEditorStyle::GetStyleSetName(), #endif TEXT("Icons.Toolbar.Play"))); TSharedRef LabelSelectionBox = SNew(SComboButton) .OnGetMenuContent(this, &FSUDSEditorToolkit::GetStartLabelMenu) .ButtonContent() [ SNew(STextBlock) .ToolTipText( INVTEXT("Choose where to start dialogue from") ) .Text(this, &FSUDSEditorToolkit::GetSelectedStartLabel ) ]; ToolbarBuilder.AddWidget(LabelSelectionBox); ToolbarBuilder.AddWidget(SNew(STextBlock) .Text(FText::FromString(TEXT("Reset Variables:"))) .Margin(FMargin(10, 7, 10, 5))); TSharedPtr ResetTooltip = IDocumentation::Get()->CreateToolTip( FText::FromString(TEXT( "Whether to reset variable state created by the script (note: vars manually created in this editor are never reset, delete them yourself)")), NULL, "", TEXT("ResetVars")); TSharedRef ResetVarsCheckbox = SNew(SCheckBox) .ToolTip(ResetTooltip) .IsChecked(this, &FSUDSEditorToolkit::GetResetVarsCheckState) .OnCheckStateChanged(this, &FSUDSEditorToolkit::OnResetVarsCheckStateChanged); ToolbarBuilder.AddWidget(ResetVarsCheckbox); } ToolbarBuilder.EndSection(); ToolbarBuilder.AddSeparator(); ToolbarBuilder.BeginSection("CachedState"); { ToolbarBuilder.AddToolBarButton(FSUDSToolbarCommands::Get().WriteBackTextIDs, NAME_None, TAttribute(), TAttribute(), FSlateIcon( #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 FAppStyle::GetAppStyleSetName(), #else FEditorStyle::GetStyleSetName(), #endif TEXT("Icons.Toolbar.Details"))); ToolbarBuilder.AddToolBarButton(FSUDSToolbarCommands::Get().GenerateVOAssets, NAME_None, TAttribute(), TAttribute(), FSlateIcon( #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 FAppStyle::GetAppStyleSetName(), #else FEditorStyle::GetStyleSetName(), #endif TEXT("Icons.Toolbar.Export"))); } ToolbarBuilder.EndSection(); } TSharedRef FSUDSEditorToolkit::GetStartLabelMenu() { FMenuBuilder MenuBuilder(true, NULL); const FUIAction StartAction(FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::OnStartLabelSelected, FName(NAME_None))); MenuBuilder.AddMenuEntry(INVTEXT("Beginning"), INVTEXT("Start from the beginning of the dialogue"), FSlateIcon(), StartAction); MenuBuilder.AddSeparator(); if (IsValid(Script)) { for (auto& LabelPair : Script->GetLabelList()) { FUIAction LabelAction(FExecuteAction::CreateSP(this, &FSUDSEditorToolkit::OnStartLabelSelected, LabelPair.Key)); MenuBuilder.AddMenuEntry(FText::FromName(LabelPair.Key), FText(), FSlateIcon(), LabelAction); } } return MenuBuilder.MakeWidget(); } void FSUDSEditorToolkit::OnStartLabelSelected(FName Label) { StartLabel = Label; } FText FSUDSEditorToolkit::GetSelectedStartLabel() const { return FText::Format(INVTEXT("From: {0}"), StartLabel.IsNone() ? INVTEXT("Beginning") : FText::FromName(StartLabel)); } ECheckBoxState FSUDSEditorToolkit::GetResetVarsCheckState() const { return bResetVarsOnStart ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } void FSUDSEditorToolkit::OnResetVarsCheckStateChanged(ECheckBoxState NewState) { bResetVarsOnStart = NewState == ECheckBoxState::Checked; } void FSUDSEditorToolkit::UnregisterTabSpawners(const TSharedRef& InTabManager) { FAssetEditorToolkit::UnregisterTabSpawners(InTabManager); InTabManager->UnregisterTabSpawner("SUDSDialogueTab"); InTabManager->UnregisterTabSpawner("SUDSVariablesTab"); InTabManager->UnregisterTabSpawner("SUDSLogTab"); } FText FSUDSEditorToolkit::GetBaseToolkitName() const { return INVTEXT("SUDS Script Editor"); } FName FSUDSEditorToolkit::GetToolkitFName() const { return "SUDSScriptEditor"; } FLinearColor FSUDSEditorToolkit::GetWorldCentricTabColorScale() const { return FLinearColor(); } FString FSUDSEditorToolkit::GetWorldCentricTabPrefix() const { return "SUDS Script "; } void FSUDSEditorToolkit::UserEditVariable(const FName& Name, FSUDSValue Value) { FName GlobalName; if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName)) { USUDSSubsystem::Test_DummyGlobalVariables.Add(GlobalName, Value); OnDialogueUserEditedVar(nullptr, Name, Value); } else { // Update manual overrides if it's one of these if (ManualOverrideVariables.Contains(Name)) { ManualOverrideVariables[Name] = Value; } // This will cause a refresh if (Dialogue) Dialogue->SetVariable(Name, Value); else { // This won't get called if dialogue is not initialised, so call it to get log & refresh OnDialogueUserEditedVar(nullptr, Name, Value); } } } void FSUDSEditorToolkit::DeleteVariable(const FName& Name) { FName GlobalName; if (USUDSLibrary::IsDialogueVariableGlobal(Name, GlobalName)) { USUDSSubsystem::Test_DummyGlobalVariables.Remove(GlobalName); } else { ManualOverrideVariables.Remove(Name); if (Dialogue) Dialogue->UnSetVariable(Name); } UpdateVariables(); } void FSUDSEditorToolkit::OnClose() { FAssetEditorToolkit::OnClose(); FReimportManager::Instance()->OnPostReimport().Remove(ReimportDelegateHandle); ReimportDelegateHandle.Reset(); DestroyDialogue(); } void FSUDSEditorToolkit::StartDialogue() { // In case the user closed important tabs EnsureTabsVisible(); Clear(); if (!Dialogue) { // Custom creation of USUDSDialogue, we want it to exist only for this window. // Mark as a root object (we'll clear it on close) { // Have to guard vs GC FGCScopeGuard GCGuard; const FName Name = MakeUniqueObjectName(GetTransientPackage(), USUDSDialogue::StaticClass(), Script->GetFName()); Dialogue = NewObject(GetTransientPackage(), Name, RF_Transient | RF_MarkAsRootSet); } Dialogue->Initialise(Script); Dialogue->InternalOnChoice.BindSP(this, &FSUDSEditorToolkit::OnDialogueChoice); Dialogue->InternalOnEvent.BindSP(this, &FSUDSEditorToolkit::OnDialogueEvent); Dialogue->InternalOnFinished.BindSP(this, &FSUDSEditorToolkit::OnDialogueFinished); Dialogue->InternalOnProceeding.BindSP(this, &FSUDSEditorToolkit::OnDialogueProceeding); Dialogue->InternalOnStarting.BindSP(this, &FSUDSEditorToolkit::OnDialogueStarting); Dialogue->InternalOnSpeakerLine.BindSP(this, &FSUDSEditorToolkit::OnDialogueSpeakerLine); Dialogue->InternalOnSetVar.BindSP(this, &FSUDSEditorToolkit::OnDialogueSetVar); Dialogue->InternalOnSetVarByCode.BindSP(this, &FSUDSEditorToolkit::OnDialogueUserEditedVar); Dialogue->InternalOnSelectEval.BindSP(this, &FSUDSEditorToolkit::OnDialogueSelectEval); } else { // Reset things manually so we can set manual override vars if (bResetVarsOnStart) Dialogue->ResetState(); } // Set manual override vars before we start for (auto& Pair : ManualOverrideVariables) { Dialogue->SetVariable(Pair.Key, Pair.Value); } Dialogue->Restart(false, StartLabel); UpdateVariables(); } void FSUDSEditorToolkit::Clear() { OutputRows.Empty(); if (OutputListView.IsValid()) { OutputListView->RequestListRefresh(); } VariableRows.Empty(); if (VariablesListView.IsValid()) { VariablesListView->RequestListRefresh(); } //TraceLog->ClearMessages(); } void FSUDSEditorToolkit::DestroyDialogue() { if (Dialogue) { // Handle garbage collection of our UObject { FGCScopeGuard GCGuard; Dialogue->RemoveFromRoot(); Dialogue->MarkAsGarbage(); } Dialogue = nullptr; CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } } void FSUDSEditorToolkit::UpdateOutput() { if (OutputListView.IsValid()) { OutputListView->RequestListRefresh(); OutputListView->ScrollToBottom(); } } void FSUDSEditorToolkit::UpdateChoiceButtons() { USUDSDialogue* D = Dialogue; ChoicesBox->ClearChildren(); if (!D->IsEnded()) { if (D->IsSimpleContinue()) { ChoicesBox->AddSlot() .Padding(0, 0 ,0 , 5) [ SNew(SButton) .HAlign(HAlign_Left) .Text(INVTEXT("(Continue...)")) .OnClicked_Lambda([D]() { D->Continue(); return FReply::Handled(); }) ]; } else { for (int i = 0; i < D->GetNumberOfChoices(); ++i) { ChoicesBox->AddSlot() .Padding(0, 0 ,0 , 5) [ SNew(SButton) .HAlign(HAlign_Left) .Text(D->GetChoiceText(i)) .OnClicked_Lambda([D, i]() { D->Choose(i); return FReply::Handled(); }) ]; } } } } void FSUDSEditorToolkit::OnDialogueChoice(USUDSDialogue* D, int ChoiceIndex, int LineNo) { if (!D->IsSimpleContinue()) { AddDialogueStep(NAME_Choice, LineNo, FText::Format(INVTEXT("[{1}] {0}"), D->GetChoiceText(ChoiceIndex), ChoiceIndex), INVTEXT("Choice")); } } void FSUDSEditorToolkit::OnDialogueEvent(USUDSDialogue* D, FName EventName, const TArray& Args, int LineNo) { #if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 TStringBuilder<256> B; #else FStringBuilderBase B; #endif if (Args.Num() > 0) { for (auto& Arg : Args) { if (B.Len() > 0) { B.Appendf(TEXT(", %s"), *Arg.ToString()); } else { B.Append("( "); B.Append(Arg.ToString()); } } B.Append(" )"); } FText ArgText = FText::FromString(B.ToString()); AddDialogueStep(NAME_Event, LineNo, FText::FormatOrdered(INVTEXT("{0} {1}"), FText::FromName(EventName), ArgText), INVTEXT("Event")); } void FSUDSEditorToolkit::OnDialogueFinished(USUDSDialogue* D) { AddDialogueStep(NAME_Finish, 0, INVTEXT("Dialogue Finished"), INVTEXT("Finish")); UpdateChoiceButtons(); } void FSUDSEditorToolkit::OnDialogueProceeding(USUDSDialogue* D) { } void FSUDSEditorToolkit::OnDialogueStarting(USUDSDialogue* D, FName LabelName) { FString LabelStr = LabelName.IsNone() ? FString("beginning") : LabelName.ToString(); AddDialogueStep(NAME_Start, 0, FText::Format(INVTEXT("Starting from {0}"), FText::FromString(LabelStr)), INVTEXT("Start")); } void FSUDSEditorToolkit::OnDialogueSpeakerLine(USUDSDialogue* D, int LineNo) { AddDialogueStep(NAME_SpeakerLine, LineNo , D->GetText(),D->GetSpeakerDisplayName()); UpdateChoiceButtons(); } void FSUDSEditorToolkit::AddOutputRow(const FText& Prefix, const FText& Line, const FSlateColor& PrefixColour, const FSlateColor& LineColour) { // Stupid table changes any colours I give it, white = dark grey // Can't figure out where it's coming from, I f**king hate Slate const FSlateColor BgColour = (OutputRows.Num() % 2) > 0 ? FSlateColor(FLinearColor::White) : FSlateColor(FLinearColor(0.9f, 0.9f, 0.9f, 1)); OutputRows.Add(MakeShareable( new FSUDSEditorOutputRow(Prefix, Line, PrefixColour, LineColour, BgColour))); UpdateOutput(); } void FSUDSEditorToolkit::AddTraceLogRow(const FName& Category, int SourceLineNo, const FString& Message) { FSlateColor Colour = GetColourForCategory(Category); if (TraceLog) { TraceLog->AppendMessage(Category, SourceLineNo, Message, Colour); } } FSlateColor FSUDSEditorToolkit::GetColourForCategory(const FName& Category) { if (Category == NAME_SpeakerLine) { return SpeakerColour; } else if (Category == NAME_Choice) { return ChoiceColour; } else if (Category == NAME_Event) { return EventColour; } else if (Category == NAME_VariableSet) { return VarSetColour; } else if (Category == NAME_VariableEdit) { return VarEditColour; } else if (Category == NAME_Start) { return StartColour; } else if (Category == NAME_Finish) { return FinishColour; } else if (Category == NAME_SelectEval) { return SelectColour; } return FSlateColor::UseForeground(); } void FSUDSEditorToolkit::AddDialogueStep(const FName& Category, int SourceLineNo, const FText& Description, const FText& Prefix) { if (Category == NAME_SpeakerLine) { AddOutputRow(Prefix, Description, SpeakerColour, FSlateColor::UseForeground()); AddTraceLogRow(Category, SourceLineNo, FString::Printf(TEXT("%s: %s"), *Prefix.ToString(), *Description.ToString())); } else if (Category == NAME_Choice) { AddOutputRow(Prefix, Description, ChoiceColour, ChoiceColour); AddTraceLogRow(Category, SourceLineNo, Description.ToString()); } else if (Category == NAME_Start) { AddOutputRow(Prefix, Description, StartColour, StartColour); AddTraceLogRow(Category, SourceLineNo, Description.ToString()); } else if (Category == NAME_Finish) { AddOutputRow(Prefix, Description, FinishColour, FinishColour); AddTraceLogRow(Category, SourceLineNo, Description.ToString()); } else { AddTraceLogRow(Category, SourceLineNo, Description.ToString()); } } FReply FSUDSEditorToolkit::AddVariableClicked() { TSharedRef Window = SNew(SWindow) .Title(INVTEXT("Add Variable")) .IsPopupWindow(true) .SupportsMaximize(false) .SupportsMinimize(false) .CreateTitleBar(true) .SizingRule(ESizingRule::Autosized) .FocusWhenFirstShown(true) .MinWidth(400) .ActivationPolicy(EWindowActivationPolicy::FirstShown); bool bConfirmed = false; ESUDSValueType ValType = ESUDSValueType::Int; FText VarNameText = FText::FromString("NewVar"); Window->SetContent( SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(0.4) [ SNew(STextBlock) .Text(INVTEXT("Name")) ] +SHorizontalBox::Slot() .FillWidth(0.6) [ SNew(SEditableTextBox) .MinDesiredWidth(150) .SelectAllTextWhenFocused(true) .Text(VarNameText) .OnTextCommitted_Lambda([&VarNameText](const FText& NewText, ETextCommit::Type CommitType) { VarNameText = NewText; }) ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(0.4) [ SNew(STextBlock) .Text(INVTEXT("Type")) ] +SHorizontalBox::Slot() .FillWidth(0.6) [ SNew(SComboButton) .OnGetMenuContent_Lambda([&ValType] { FMenuBuilder MenuBuilder(true, NULL); // NumEnums() - 1 because the last value is the autogen _MAX value for (int i = 0; i < StaticEnum()->NumEnums() - 1; ++i) { ESUDSValueType Val = (ESUDSValueType)StaticEnum()->GetValueByIndex(i); if (Val != ESUDSValueType::Empty && Val != ESUDSValueType::Variable) { const FUIAction Action(FExecuteAction::CreateLambda([Val, &ValType]() { ValType = Val; })); MenuBuilder.AddMenuEntry(FText::FromString(StaticEnum()->GetNameStringByIndex(i)), FText(), FSlateIcon(), Action); } } return MenuBuilder.MakeWidget(); }) .ButtonContent() [ SNew(STextBlock) .Text_Lambda([&ValType] { return FText::FromString(StaticEnum()->GetNameStringByValue((int64)ValType)); }) ] ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(0, 5, 0, 3) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .HAlign(HAlign_Right) .Padding(2) [ SNew(SButton) .Text(INVTEXT("OK")) .OnClicked_Lambda([&bConfirmed, Window, VarNameText]() { if (!VarNameText.IsEmptyOrWhitespace()) { bConfirmed = true; Window->RequestDestroyWindow(); } return FReply::Handled(); }) ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) .AutoWidth() .Padding(2) [ SNew(SButton) .Text(INVTEXT("Cancel")) .OnClicked_Lambda([&bConfirmed, Window]() { bConfirmed = false; Window->RequestDestroyWindow(); return FReply::Handled(); }) ] ] ); FVector2D Pos = FSlateApplication::Get().GetCursorPos(); Pos.X -= 230; Pos.Y += 100; Window->MoveWindowTo(Pos); // This doesn't return until closed GEditor->EditorAddModalWindow(Window); if (bConfirmed) { FName VarName(VarNameText.ToString().TrimStartAndEnd()); const FSUDSValue Val(ValType); FName GlobalVarName; if (USUDSLibrary::IsDialogueVariableGlobal(VarName, GlobalVarName)) { USUDSSubsystem::Test_DummyGlobalVariables.Add(GlobalVarName, Val); OnDialogueUserEditedVar(nullptr, VarName, Val); } else { ManualOverrideVariables.Add(VarName, Val); if (Dialogue) { // This will cause a refresh Dialogue->SetVariable(VarName, Val); } else { // This won't get called if dialogue is not initialised, so call it to get log & refresh OnDialogueUserEditedVar(nullptr, VarName, Val); } } } return FReply::Handled(); } void FSUDSEditorToolkit::UpdateVariables() { VariableRows.Empty(); // From dialogue (will contain manual overrides already) if (Dialogue) { for (auto& Pair : Dialogue->GetVariables()) { VariableRows.Add(MakeShareable( new FSUDSEditorVariableRow(Pair.Key, Pair.Value, ManualOverrideVariables.Contains(Pair.Key)))); } } else { // Manual overrides can occur before dialogue is started // Otherwise, they will be in the dialogue for (auto& Pair : ManualOverrideVariables) { VariableRows.Add(MakeShareable(new FSUDSEditorVariableRow(Pair.Key, Pair.Value, true))); } } // Also do global vars for (auto& Pair : USUDSSubsystem::Test_DummyGlobalVariables) { VariableRows.Add(MakeShareable( new FSUDSEditorVariableRow( // We need to insert the "global." prefix back in since it's removed in the global var list FName(FString::Printf(TEXT("global.%s"), *Pair.Key.ToString())), Pair.Value, true))); } VariableRows.Sort(); if (VariablesListView.IsValid()) { VariablesListView->RequestListRefresh(); } } void FSUDSEditorToolkit::OnPostReimport(UObject* Object, bool bSuccess) { if (Object == Script) { // Destroy any dialogue instance, will not be valid post-import DestroyDialogue(); Clear(); } } void FSUDSEditorToolkit::OnDialogueSetVar(USUDSDialogue* D, FName VariableName, const FSUDSValue& ToValue, const FString& ExpressionStr, int LineNo) { FText Description; if (ExpressionStr.Len() > 0) { Description = FText::Format(INVTEXT("{0} = {1} = {2}"), FText::FromName(VariableName), FText::FromString(ExpressionStr), FText::FromString(ToValue.ToString())); } else { Description = FText::Format(INVTEXT("{0} = {1}"), FText::FromName(VariableName), FText::FromString(ToValue.ToString())); } AddDialogueStep(NAME_VariableSet, LineNo, Description, INVTEXT("Set Variable")); UpdateVariables(); } void FSUDSEditorToolkit::OnDialogueUserEditedVar(USUDSDialogue* D, FName VariableName, const FSUDSValue& ToValue) { // User-edited value const FText Description = FText::Format(INVTEXT("{0} = {1}"), FText::FromName(VariableName), FText::FromString(ToValue.ToString())); AddDialogueStep(NAME_VariableEdit, 0, Description, INVTEXT("Edit Variable")); UpdateVariables(); } void FSUDSEditorToolkit::OnDialogueSelectEval(USUDSDialogue* D, const FString& ExpressionStr, bool bSuccess, int LineNo) { AddDialogueStep(NAME_SelectEval, LineNo, FText::Format(INVTEXT("{0} = {1}"), FText::FromString(ExpressionStr), bSuccess ? INVTEXT("true") : INVTEXT("false")), INVTEXT("Conditional")); } void FSUDSEditorToolkit::OnDialogueRandomEval(USUDSDialogue* D, const int RandomOutcome, int LineNo) { AddDialogueStep(NAME_RandomEval, LineNo, FText::Format(INVTEXT(": {0}"), RandomOutcome), INVTEXT("Random")); } TSharedRef FSUDSEditorToolkit::OnGenerateRowForOutput( TSharedPtr Row, const TSharedRef& OwnerTable) { return SNew( SSUDSEditorOutputItem, OwnerTable ) .Prefix(Row->Prefix) .PrefixColour(Row->PrefixColour) .Line(Row->Line) .LineColour(Row->LineColour) .BgColour(Row->BgColour) .InitialWidth( PrefixColumnWidth ); } TSharedRef FSUDSEditorToolkit::OnGenerateRowForVariable(TSharedPtr Row, const TSharedRef& Table) { return SNew( SSUDSEditorVariableItem, Table ) .VariableName(Row->Name) .VariableValue(Row->Value) .bIsManualOverride(Row->bIsManualOverride) .Parent(this) .InitialWidth( VarColumnWidth ); } void FSUDSEditorToolkit::WriteBackTextIDs() { if (FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString( "Are you sure you want to write string keys back to this script?")) == EAppReturnType::Yes) { FSUDSMessageLogger::ClearMessages(); FSUDSMessageLogger Logger; FSUDSEditorScriptTools::WriteBackTextIDs(Script, Logger); } } void FSUDSEditorToolkit::GenerateVOAssets() { if (FMessageDialog::Open(EAppMsgType::YesNo, FText::FromString( "Are you sure you want to generate Dialogue Voice / Dialogue Wave assets for this script?")) == EAppReturnType::Yes) { EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transactional; FSUDSMessageLogger::ClearMessages(); FSUDSMessageLogger Logger; FSUDSEditorVoiceOverTools::GenerateAssets(Script, Flags, &Logger); } } void SSUDSEditorVariableItem::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { InitialWidth = InArgs._InitialWidth; VariableName = InArgs._VariableName; VariableValue = InArgs._VariableValue; bIsManualOverride = InArgs._bIsManualOverride; Parent = InArgs._Parent; SMultiColumnTableRow< TSharedPtr< FString > >::Construct( SMultiColumnTableRow< TSharedPtr< FString > >::FArguments(), InOwnerTableView ); } TSharedRef SSUDSEditorVariableItem::GenerateWidgetForColumn(const FName& ColumnName) { #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 const FSlateFontInfo PropertyFont = FAppStyle::GetFontStyle(TEXT("PropertyWindow.NormalFont")); const FSlateBrush* BorderBrush = FAppStyle::GetBrush("ToolPanel.GroupBorder"); #else const FSlateFontInfo PropertyFont = FEditorStyle::GetFontStyle(TEXT("PropertyWindow.NormalFont")); const FSlateBrush* BorderBrush = FEditorStyle::GetBrush("ToolPanel.GroupBorder"); #endif const FSlateColor NormalBgColour = FSlateColor(FLinearColor::White); const FSlateColor ManualOverrideBgColour = FSlateColor(FLinearColor(0.9f,0.7f,1,1)); if (ColumnName == "NameHeader") { const FMargin BoxMargin(0, 2, 0, 1.333f); return SNew(SBorder) .BorderImage(BorderBrush) .BorderBackgroundColor(bIsManualOverride ? ManualOverrideBgColour : NormalBgColour) .Padding(BoxMargin) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0) .Padding(10, 5, 5, 5) .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(PropertyFont) .Text(FText::FromName(VariableName)) ] ]; } else { TSharedPtr ValueWidget; FMargin InnerPadding = FMargin(5, 2, 5, 2); switch (VariableValue.GetType()) { case ESUDSValueType::Int: ValueWidget = SNew(SNumericEntryBox) .Value(VariableValue.GetIntValue()) .OnValueCommitted_Lambda([this] (const int32 InValue, ETextCommit::Type) { Parent->UserEditVariable(VariableName, InValue); }) .MinDesiredValueWidth(60) .Font(PropertyFont); break; case ESUDSValueType::Float: ValueWidget = SNew(SNumericEntryBox) .Value(VariableValue.GetFloatValue()) .OnValueCommitted_Lambda([this] (const float InValue, ETextCommit::Type) { Parent->UserEditVariable(VariableName, InValue); }) .MinDesiredValueWidth(60) .Font(PropertyFont); break; case ESUDSValueType::Boolean: ValueWidget = SNew(SCheckBox) .IsChecked(VariableValue.GetBooleanValue() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked) .OnCheckStateChanged_Lambda([this] (ECheckBoxState NewState) { Parent->UserEditVariable(VariableName, NewState == ECheckBoxState::Checked); }); InnerPadding.Top = InnerPadding.Bottom = 2; break; case ESUDSValueType::Gender: ValueWidget = SNew(SComboButton) .OnGetMenuContent(this, &SSUDSEditorVariableItem::GetGenderMenu) .ButtonContent() [ SNew(STextBlock) .Text(FText::FromString(VariableValue.ToString())) .Font(PropertyFont) ]; InnerPadding.Top = InnerPadding.Bottom = 1; break; case ESUDSValueType::Text: ValueWidget = SNew(SEditableTextBox) .IsReadOnly(false) .Text(VariableValue.GetTextValue()) .Font(PropertyFont) .OnTextCommitted_Lambda([this] (const FText& InValue, ETextCommit::Type) { Parent->UserEditVariable(VariableName, InValue); }); break; case ESUDSValueType::Name: ValueWidget = SNew(SEditableTextBox) .IsReadOnly(false) .Text(FText::FromString(VariableValue.GetNameValue().ToString())) .Font(PropertyFont) .OnTextCommitted_Lambda([this] (const FText& InValue, ETextCommit::Type) { Parent->UserEditVariable(VariableName, FSUDSValue(FName(InValue.ToString()), false)); }); break; case ESUDSValueType::Variable: case ESUDSValueType::Empty: default: ValueWidget = SNew(STextBlock); break; }; const FMargin BoxMargin(2, 1, 0, 1); return SNew(SBox) .HeightOverride(26) .Padding(BoxMargin) [ SNew(SBorder) .BorderImage(BorderBrush) .BorderBackgroundColor(bIsManualOverride ? ManualOverrideBgColour : NormalBgColour) .VAlign(VAlign_Center) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(InnerPadding) .AutoWidth() [ ValueWidget.ToSharedRef() ] + SHorizontalBox::Slot() .Padding(5,2,5,2) .FillWidth(1.0) .HAlign(HAlign_Right) [ SNew(SButton) .ButtonStyle( FAppStyle::Get(), "SimpleButton" ) .OnClicked_Lambda([this] { // Delete click Parent->DeleteVariable(VariableName); return FReply::Handled(); } ) .ContentPadding(0) .IsFocusable(false) [ SNew( SImage ) #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 .Image( FAppStyle::GetBrush("Icons.Delete") ) #else .Image( FEditorStyle::GetBrush("Icons.Delete") ) #endif .ColorAndOpacity( FSlateColor::UseForeground() ) ] ] ] ]; } } TSharedRef SSUDSEditorVariableItem::GetGenderMenu() { FMenuBuilder MenuBuilder(true, NULL); #if ENGINE_MAJOR_VERSION ==5 && ENGINE_MINOR_VERSION >= 8 // StaticEnum() doesn't exist anymore so we have to do this manually const FUIAction Action0(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, ETextGender::Feminine)); MenuBuilder.AddMenuEntry(FText::FromString(LexToString(ETextGender::Feminine)), FText(), FSlateIcon(), Action0); const FUIAction Action1(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, ETextGender::Masculine)); MenuBuilder.AddMenuEntry(FText::FromString(LexToString(ETextGender::Masculine)), FText(), FSlateIcon(), Action1); const FUIAction Action2(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, ETextGender::Neuter)); MenuBuilder.AddMenuEntry(FText::FromString(LexToString(ETextGender::Neuter)), FText(), FSlateIcon(), Action2); #else // NumEnums() - 1 because the last value is the autogen _MAX value for (int i = 0; i < StaticEnum()->NumEnums() - 1; ++i) { ETextGender Val = (ETextGender)StaticEnum()->GetValueByIndex(i); const FUIAction Action(FExecuteAction::CreateSP(this, &SSUDSEditorVariableItem::OnGenderSelected, Val)); MenuBuilder.AddMenuEntry(FText::FromString(StaticEnum()->GetNameStringByIndex(i)), FText(), FSlateIcon(), Action); } #endif return MenuBuilder.MakeWidget(); } void SSUDSEditorVariableItem::OnGenderSelected(ETextGender NewGender) { Parent->UserEditVariable(VariableName, NewGender); } FVector2D SSUDSEditorVariableItem::ComputeDesiredSize(float X) const { // ItemHeight(28) at table level is totally not working for some reason, so fudge it here FVector2D Desired = SMultiColumnTableRow>::ComputeDesiredSize(X); Desired.Y = 28; return Desired; } void SSUDSEditorOutputItem::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { InitialWidth = InArgs._InitialWidth; Prefix = InArgs._Prefix; PrefixColour = InArgs._PrefixColour; Line = InArgs._Line; LineColour = InArgs._LineColour; SetBorderBackgroundColor(InArgs._BgColour); SMultiColumnTableRow< TSharedPtr< FString > >::Construct( SMultiColumnTableRow< TSharedPtr< FString > >::FArguments(), InOwnerTableView ); } TSharedRef SSUDSEditorOutputItem::GenerateWidgetForColumn(const FName& ColumnName) { if (ColumnName == "PrefixHeader") { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0) .Padding(10, 5, 10, 5) [ SNew(STextBlock) .Text(Prefix) .ColorAndOpacity(PrefixColour) ]; } else { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0) .Padding(10, 5, 10, 5) [ SNew(STextBlock) .Text(Line) .AutoWrapText(true) .ColorAndOpacity(LineColour) ]; } } FSUDSTraceLogMarshaller::FSUDSTraceLogMarshaller() { } void FSUDSTraceLogMarshaller::SetText(const FString& SourceString, FTextLayout& TargetTextLayout) { static const FName LogNormalStyle(TEXT("Log.Normal")); #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 const FTextBlockStyle& OrigStyle = FAppStyle::Get().GetWidgetStyle(LogNormalStyle); #else const FTextBlockStyle& OrigStyle = FEditorStyle::Get().GetWidgetStyle(LogNormalStyle); #endif for (const auto& Msg : Messages) { // Get base style & copy FTextBlockStyle Style = OrigStyle; Style.ColorAndOpacity = Msg->Colour; TArray> Runs; Runs.Add(FSlateTextRun::Create(FRunInfo(), Msg->Message, Style)); TargetTextLayout.AddLine(FTextLayout::FNewLineData(Msg->Message, Runs)); } } void FSUDSTraceLogMarshaller::GetText(FString& TargetString, const FTextLayout& SourceTextLayout) { SourceTextLayout.GetAsText(TargetString); } void FSUDSTraceLogMarshaller::AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour) { const int MinCategorySize = 12; const FString CatStr = InCategory.ToString(); int CatLen = CatStr.Len(); const FString CatPadding = CatLen < MinCategorySize ? FString::ChrN(MinCategorySize - CatLen, ' ') : ""; const FString LineNoStr = LineNo > 0 ? FString::Printf(TEXT("L%04d"), LineNo) : " "; const FString ConcatLine = FString::Printf(TEXT("%s[%s] %s %s"), *CatPadding, *CatStr, *LineNoStr, *Message); Messages.Add(MakeShareable(new FSUDSTraceLogMessage(InCategory, ConcatLine, Colour))); MakeDirty(); } void FSUDSTraceLogMarshaller::ClearMessages() { Messages.Empty(); MakeDirty(); } void SSUDSTraceLog::Construct(const FArguments& InArgs) { TraceLogMarshaller = MakeShareable(new FSUDSTraceLogMarshaller()); TraceLogTextBox = SNew(SMultiLineEditableTextBox) #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION > 0 .Style(FAppStyle::Get(), "Log.TextBox") #else .Style(FEditorStyle::Get(), "Log.TextBox") .TextStyle(FEditorStyle::Get(), "Log.Normal") #endif .Marshaller(TraceLogMarshaller) .IsReadOnly(true) .AutoWrapText(true) .OnVScrollBarUserScrolled(this, &SSUDSTraceLog::OnUserScrolled) .AlwaysShowScrollbars(true); ChildSlot .Padding(3) [ SNew(SVerticalBox) // could add a filter here like output log // log area +SVerticalBox::Slot() .FillHeight(1) [ TraceLogTextBox.ToSharedRef() ] ]; } void SSUDSTraceLog::OnUserScrolled(float X) { // revert to auto scroll when near the bottom bIsUserScrolled = X < (1.0 - SMALL_NUMBER); } void SSUDSTraceLog::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); // We seem to have to do this in Tick(), trying to do it after appending never works if (!bIsUserScrolled) { ScrollToEnd(); } } void SSUDSTraceLog::AppendMessage(FName InCategory, int LineNo, const FString& Message, const FSlateColor& Colour) { TraceLogMarshaller->AppendMessage(InCategory, LineNo, Message, Colour); } void SSUDSTraceLog::ClearMessages() { TraceLogMarshaller->ClearMessages(); } void SSUDSTraceLog::ScrollToEnd() { TraceLogTextBox->ScrollTo(ETextLocation::EndOfDocument); bIsUserScrolled = false; }