762 lines
21 KiB
C++
762 lines
21 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SPCGNodeSourceTextBox.h"
|
|
|
|
#include "Compute/IPCGNodeSourceTextProvider.h"
|
|
|
|
#include "PCGEditorStyle.h"
|
|
#include "SPCGShaderTextSearchWidget.h"
|
|
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Framework/Application/SlateUser.h"
|
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "Widgets/Layout/SGridPanel.h"
|
|
#include "Widgets/Layout/SScrollBar.h"
|
|
#include "Widgets/Text/SMultiLineEditableText.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "PCGShaderTextDocumentTextBox"
|
|
|
|
static int32 GetNumSpacesAtStartOfLine(const FString& InLine)
|
|
{
|
|
int32 NumSpaces = 0;
|
|
for (const TCHAR Char : InLine)
|
|
{
|
|
if ((Char != TEXT(' ')))
|
|
{
|
|
break;
|
|
}
|
|
|
|
NumSpaces++;
|
|
}
|
|
|
|
return NumSpaces;
|
|
}
|
|
|
|
static bool IsOpenBrace(const TCHAR& InCharacter)
|
|
{
|
|
return (InCharacter == TEXT('{') || InCharacter == TEXT('[') || InCharacter == TEXT('('));
|
|
}
|
|
|
|
static bool IsCloseBrace(const TCHAR& InCharacter)
|
|
{
|
|
return (InCharacter == TEXT('}') || InCharacter == TEXT(']') || InCharacter == TEXT(')'));
|
|
}
|
|
|
|
static TCHAR GetMatchedCloseBrace(const TCHAR& InCharacter)
|
|
{
|
|
return InCharacter == TEXT('{') ? TEXT('}') :
|
|
(InCharacter == TEXT('[') ? TEXT(']') : TEXT(')'));
|
|
}
|
|
|
|
static bool IsWhiteSpace(const TCHAR& InCharacter)
|
|
{
|
|
return InCharacter == TEXT(' ') || InCharacter == TEXT('\r') || InCharacter == TEXT('\n');
|
|
}
|
|
|
|
FPCGNodeSourceEditorTextBoxCommands::FPCGNodeSourceEditorTextBoxCommands()
|
|
: TCommands<FPCGNodeSourceEditorTextBoxCommands>(
|
|
"PCGShaderTextEditorDocumentTextBox", // Context name for fast lookup
|
|
NSLOCTEXT("Contexts", "PCGShaderTextEditorDocumentTextBox", "PCG HLSL Source Editor Document TextBox"), // Localized context name for display
|
|
NAME_None,
|
|
FAppStyle::GetAppStyleSetName()
|
|
)
|
|
{
|
|
}
|
|
|
|
void FPCGNodeSourceEditorTextBoxCommands::RegisterCommands()
|
|
{
|
|
UI_COMMAND(ApplyChanges, "Apply Changes", "Apply Changes", EUserInterfaceActionType::Button, FInputChord(EKeys::Enter, EModifierKey::Alt));
|
|
|
|
UI_COMMAND(Search, "Search", "Search for a String", EUserInterfaceActionType::Button, FInputChord(EKeys::F, EModifierKey::Control));
|
|
UI_COMMAND(NextOccurrence, "Next Occurrence", "Go to Next Occurrence", EUserInterfaceActionType::Button, FInputChord(EKeys::F3, EModifierKey::None));
|
|
UI_COMMAND(PreviousOccurrence, "Previous Occurrence", "Go to Previous Occurrence", EUserInterfaceActionType::Button, FInputChord(EKeys::F3, EModifierKey::Shift));
|
|
|
|
UI_COMMAND(ToggleComment, "Toggle Comment", "Comment/Uncomment selected lines", EUserInterfaceActionType::Button, FInputChord(EKeys::Slash, EModifierKey::Control));
|
|
}
|
|
|
|
SPCGNodeSourceTextBox::SPCGNodeSourceTextBox()
|
|
: bIsSearchBarHidden(true)
|
|
, TopLevelCommandList(MakeShared<FUICommandList>())
|
|
, TextCommandList(MakeShared<FUICommandList>())
|
|
{
|
|
}
|
|
|
|
SPCGNodeSourceTextBox::~SPCGNodeSourceTextBox()
|
|
{
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::Construct(const FArguments& InArgs)
|
|
{
|
|
RegisterCommands();
|
|
|
|
OnTextChangesAppliedCallback = InArgs._OnTextChangesApplied;
|
|
|
|
const TSharedPtr<SScrollBar> HScrollBar = SNew(SScrollBar)
|
|
.Orientation(EOrientation::Orient_Horizontal);
|
|
|
|
const TSharedPtr<SScrollBar> VScrollBar = SNew(SScrollBar)
|
|
.Orientation(EOrientation::Orient_Vertical);
|
|
|
|
const FTextBlockStyle& TextStyle = FAppStyle::Get().GetWidgetStyle<FTextBlockStyle>("MessageLog");
|
|
const FSlateFontInfo &Font = TextStyle.Font;
|
|
|
|
const bool bReadOnly = InArgs._IsReadOnly.Get();
|
|
const FPCGNodeSourceEditorTextBoxCommands& Commands = FPCGNodeSourceEditorTextBoxCommands::Get();
|
|
|
|
Text = SNew(SMultiLineEditableText)
|
|
.Font(Font)
|
|
.TextStyle(&TextStyle)
|
|
.Text(InArgs._Text)
|
|
.OnTextChanged(InArgs._OnTextChanged)
|
|
.OnTextCommitted(InArgs._OnTextCommitted)
|
|
.OnKeyCharHandler(this, &SPCGNodeSourceTextBox::OnTextKeyChar)
|
|
.OnKeyDownHandler(this, &SPCGNodeSourceTextBox::OnTextKeyDown)
|
|
// By default, the Tab key gets routed to "next widget". We want to disable that behaviour.
|
|
.OnIsTypedCharValid_Lambda([](const TCHAR InChar) { return true; })
|
|
.Marshaller(InArgs._Marshaller)
|
|
.AutoWrapText(false)
|
|
.ClearTextSelectionOnFocusLoss(false)
|
|
.AllowContextMenu(true)
|
|
.ContextMenuExtender(
|
|
FMenuExtensionDelegate::CreateLambda(
|
|
[this, bReadOnly, Commands](FMenuBuilder& InBuilder)
|
|
{
|
|
if (!bReadOnly)
|
|
{
|
|
InBuilder.PushCommandList(TextCommandList);
|
|
InBuilder.AddMenuEntry(Commands.ApplyChanges);
|
|
InBuilder.AddMenuEntry(Commands.ToggleComment);
|
|
InBuilder.PopCommandList();
|
|
}
|
|
|
|
}))
|
|
.IsReadOnly(this, &SPCGNodeSourceTextBox::IsReadOnly)
|
|
.HScrollBar(HScrollBar)
|
|
.VScrollBar(VScrollBar);
|
|
|
|
SearchBar = SNew(SPCGShaderTextSearchWidget)
|
|
.OnTextChanged(this, &SPCGNodeSourceTextBox::OnSearchTextChanged)
|
|
.OnTextCommitted(this, &SPCGNodeSourceTextBox::OnSearchTextCommitted)
|
|
.SearchResultData(this, &SPCGNodeSourceTextBox::GetSearchResultData)
|
|
.OnResultNavigationButtonClicked(this, &SPCGNodeSourceTextBox::OnSearchResultNavigationButtonClicked);
|
|
|
|
ChildSlot
|
|
[
|
|
SAssignNew(TabBody, SVerticalBox)
|
|
+SVerticalBox::Slot()
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FPCGEditorStyle::Get().GetBrush("TextEditor.Border"))
|
|
.BorderBackgroundColor(FLinearColor(0.55f, 0.55f, 0.55f, 1.0f))
|
|
[
|
|
SNew(SGridPanel)
|
|
.FillColumn(0,1.0f)
|
|
.FillRow(0,1.0f)
|
|
+SGridPanel::Slot(0,0)
|
|
[
|
|
Text.ToSharedRef()
|
|
]
|
|
+SGridPanel::Slot(1,0)
|
|
[
|
|
VScrollBar.ToSharedRef()
|
|
]
|
|
+SGridPanel::Slot(0,1)
|
|
[
|
|
HScrollBar.ToSharedRef()
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::SetTextProviderObject(UObject* InProviderObject)
|
|
{
|
|
ShaderTextProviderObject = InProviderObject;
|
|
|
|
Refresh();
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::RegisterCommands()
|
|
{
|
|
const FPCGNodeSourceEditorTextBoxCommands& Commands = FPCGNodeSourceEditorTextBoxCommands::Get();
|
|
|
|
TopLevelCommandList->MapAction(Commands.Search, FExecuteAction::CreateSP(this, &SPCGNodeSourceTextBox::OnTriggerSearch));
|
|
TopLevelCommandList->MapAction(Commands.NextOccurrence, FExecuteAction::CreateSP(this, &SPCGNodeSourceTextBox::OnGoToNextOccurrence));
|
|
TopLevelCommandList->MapAction(Commands.PreviousOccurrence, FExecuteAction::CreateSP(this, &SPCGNodeSourceTextBox::OnGoToPreviousOccurrence));
|
|
TextCommandList->MapAction(Commands.ApplyChanges, FExecuteAction::CreateSP(this, &SPCGNodeSourceTextBox::OnApplyChanges));
|
|
TextCommandList->MapAction(Commands.ToggleComment, FExecuteAction::CreateSP(this, &SPCGNodeSourceTextBox::OnToggleComment));
|
|
}
|
|
|
|
FReply SPCGNodeSourceTextBox::OnPreviewKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
|
|
{
|
|
const FKey Key = InKeyEvent.GetKey();
|
|
|
|
if (Key == EKeys::Escape)
|
|
{
|
|
if (HandleEscape())
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
|
|
if (TopLevelCommandList->ProcessCommandBindings(InKeyEvent))
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return SCompoundWidget::OnPreviewKeyDown(MyGeometry, InKeyEvent);
|
|
}
|
|
|
|
bool SPCGNodeSourceTextBox::HandleEscape()
|
|
{
|
|
if (HideSearchBar())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::ShowSearchBar()
|
|
{
|
|
if (bIsSearchBarHidden)
|
|
{
|
|
bIsSearchBarHidden = false;
|
|
|
|
TabBody->InsertSlot(0)
|
|
.AutoHeight()
|
|
[
|
|
SearchBar.ToSharedRef()
|
|
];
|
|
}
|
|
}
|
|
|
|
bool SPCGNodeSourceTextBox::HideSearchBar()
|
|
{
|
|
if (!bIsSearchBarHidden)
|
|
{
|
|
bIsSearchBarHidden = true;
|
|
SearchBar->ClearSearchText();
|
|
TabBody->RemoveSlot(SearchBar.ToSharedRef());
|
|
|
|
FSlateApplication::Get().ForEachUser([&](FSlateUser& User) {
|
|
User.SetFocus(Text.ToSharedRef(), EFocusCause::SetDirectly);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnTriggerSearch()
|
|
{
|
|
ShowSearchBar();
|
|
|
|
FText SelectedText = Text->GetSelectedText();
|
|
|
|
SearchBar->TriggerSearch(SelectedText);
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::Refresh() const
|
|
{
|
|
check(IsInGameThread());
|
|
Text->Refresh();
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnSearchTextChanged(const FText& InTextToSearch)
|
|
{
|
|
// we start the search from the beginning of current selection.
|
|
// goto clears the selection, but it will be restored by the first search
|
|
Text->GoTo(Text->GetSelection().GetBeginning());
|
|
|
|
Text->SetSearchText(InTextToSearch);
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnSearchTextCommitted(const FText& InTextToSearch, ETextCommit::Type InCommitType)
|
|
{
|
|
if (!InTextToSearch.EqualTo(Text->GetSearchText()))
|
|
{
|
|
Text->SetSearchText(InTextToSearch);
|
|
}
|
|
else
|
|
{
|
|
if (InCommitType == ETextCommit::Type::OnEnter)
|
|
{
|
|
OnSearchResultNavigationButtonClicked(SSearchBox::SearchDirection::Next);
|
|
}
|
|
}
|
|
}
|
|
|
|
TOptional<SSearchBox::FSearchResultData> SPCGNodeSourceTextBox::GetSearchResultData() const
|
|
{
|
|
FText SearchText = Text->GetSearchText();
|
|
|
|
if (!SearchText.IsEmpty())
|
|
{
|
|
SSearchBox::FSearchResultData Result;
|
|
Result.CurrentSearchResultIndex = Text->GetSearchResultIndex();
|
|
Result.NumSearchResults = Text->GetNumSearchResults();
|
|
|
|
return Result;
|
|
}
|
|
|
|
return TOptional<SSearchBox::FSearchResultData>();
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnSearchResultNavigationButtonClicked(SSearchBox::SearchDirection InDirection)
|
|
{
|
|
Text->AdvanceSearch(InDirection == SSearchBox::SearchDirection::Previous);
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnGoToNextOccurrence()
|
|
{
|
|
OnSearchResultNavigationButtonClicked(SSearchBox::SearchDirection::Next);
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnGoToPreviousOccurrence()
|
|
{
|
|
OnSearchResultNavigationButtonClicked(SSearchBox::SearchDirection::Previous);
|
|
}
|
|
|
|
FReply SPCGNodeSourceTextBox::OnTextKeyDown(
|
|
const FGeometry& MyGeometry,
|
|
const FKeyEvent& InKeyEvent) const
|
|
{
|
|
if (TextCommandList->ProcessCommandBindings(InKeyEvent))
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
|
|
// Let SMultiLineEditableText::OnKeyDown handle it.
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnApplyChanges() const
|
|
{
|
|
OnTextChangesAppliedCallback.ExecuteIfBound();
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::OnToggleComment() const
|
|
{
|
|
SMultiLineEditableText::FScopedEditableTextTransaction Transaction(Text);
|
|
|
|
const FTextLocation CursorLocation = Text->GetCursorLocation();
|
|
|
|
const FTextSelection Selection = Text->GetSelection();
|
|
const FTextLocation SelectionStart =
|
|
CursorLocation == Selection.GetBeginning() ? Selection.GetEnd() : Selection.GetBeginning();
|
|
|
|
// Need to shift the selection according to the new indentation
|
|
FTextLocation NewCursorLocation = CursorLocation;
|
|
FTextLocation NewSelectionStart = SelectionStart;
|
|
|
|
const int32 StartLine = Selection.GetBeginning().GetLineIndex();
|
|
const int32 EndLine = Selection.GetEnd().GetLineIndex();
|
|
|
|
bool bShouldComment = false;
|
|
bool bAreAllLinesEmpty = true;
|
|
int32 MinNumLeadingSpaces = INDEX_NONE;
|
|
for (int32 Index = StartLine; Index <= EndLine; Index++)
|
|
{
|
|
FString Line;
|
|
Text->GetTextLine(Index, Line);
|
|
// Empty lines are not considered when deciding whether to comment/uncomment
|
|
const int32 NumSpaces = GetNumSpacesAtStartOfLine(Line);
|
|
if (NumSpaces >= Line.Len())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bAreAllLinesEmpty = false;
|
|
|
|
FStringView LineView(&Line[NumSpaces]);
|
|
if (!LineView.StartsWith(TEXT("//")))
|
|
{
|
|
bShouldComment = true;
|
|
}
|
|
|
|
MinNumLeadingSpaces =
|
|
MinNumLeadingSpaces == INDEX_NONE ?
|
|
NumSpaces : FMath::Min(MinNumLeadingSpaces, NumSpaces);
|
|
}
|
|
|
|
// Find nearest tab
|
|
MinNumLeadingSpaces = MinNumLeadingSpaces / 4 * 4;
|
|
|
|
// Single line comment or single line no-op should move the cursor down
|
|
const bool bShouldGoToNewLine = !Text->AnyTextSelected() && (bShouldComment || bAreAllLinesEmpty);
|
|
|
|
for (int32 Index = StartLine; Index <= EndLine; Index++)
|
|
{
|
|
FString Line;
|
|
Text->GetTextLine(Index, Line);
|
|
|
|
if (!Line.IsEmpty())
|
|
{
|
|
const int32 NumSpaces = GetNumSpacesAtStartOfLine(Line);
|
|
if (NumSpaces < Line.Len())
|
|
{
|
|
int32 LineShift = 0;
|
|
if (bShouldComment)
|
|
{
|
|
Text->GoTo(FTextLocation(Index, MinNumLeadingSpaces));
|
|
Text->InsertTextAtCursor(TEXT("// "));
|
|
LineShift = 3;
|
|
}
|
|
else
|
|
{
|
|
int32 CommentOffset = Line.Find("//");
|
|
if (ensure(CommentOffset != INDEX_NONE))
|
|
{
|
|
int32 ContentOffset =
|
|
Line.IsValidIndex(CommentOffset+2) && Line[CommentOffset+2] == TEXT(' ') ?
|
|
3 : 2;
|
|
Text->SelectText({Index, CommentOffset}, {Index, CommentOffset+ContentOffset});
|
|
Text->DeleteSelectedText();
|
|
LineShift = -ContentOffset;
|
|
}
|
|
}
|
|
|
|
if (Index == CursorLocation.GetLineIndex())
|
|
{
|
|
NewCursorLocation = FTextLocation(CursorLocation, LineShift);
|
|
}
|
|
|
|
if (Index == SelectionStart.GetLineIndex())
|
|
{
|
|
NewSelectionStart = FTextLocation(SelectionStart, LineShift);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bShouldGoToNewLine)
|
|
{
|
|
// Automatically go to the next line if there is no selection
|
|
int32 LineIndex = CursorLocation.GetLineIndex() + 1;
|
|
if (LineIndex < Text->GetTextLineCount())
|
|
{
|
|
FString Line;
|
|
Text->GetTextLine(LineIndex, Line);
|
|
|
|
int32 Offset = FMath::Min(CursorLocation.GetOffset(), Line.Len());
|
|
|
|
Text->GoTo({LineIndex, Offset});
|
|
}
|
|
else
|
|
{
|
|
Text->GoTo(NewCursorLocation);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Text->SelectText(NewSelectionStart, NewCursorLocation);
|
|
}
|
|
}
|
|
|
|
FReply SPCGNodeSourceTextBox::OnTextKeyChar(
|
|
const FGeometry& MyGeometry,
|
|
const FCharacterEvent& InCharacterEvent) const
|
|
{
|
|
if (Text->IsTextReadOnly())
|
|
{
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
const TCHAR Character = InCharacterEvent.GetCharacter();
|
|
|
|
if (Character == TEXT('\b'))
|
|
{
|
|
if (!Text->AnyTextSelected())
|
|
{
|
|
// if we are deleting a single open brace
|
|
// look for a matching close brace following it and delete the close brace as well
|
|
const FTextLocation CursorLocation = Text->GetCursorLocation();
|
|
const int Offset = CursorLocation.GetOffset();
|
|
FString Line;
|
|
Text->GetTextLine(CursorLocation.GetLineIndex(), Line);
|
|
|
|
if (Line.IsValidIndex(Offset) && Line.IsValidIndex(Offset-1))
|
|
{
|
|
if (IsOpenBrace(Line[Offset-1]))
|
|
{
|
|
if (Line[Offset] == GetMatchedCloseBrace(Line[Offset-1]))
|
|
{
|
|
SMultiLineEditableText::FScopedEditableTextTransaction ScopedTransaction(Text);
|
|
Text->SelectText(FTextLocation(CursorLocation, -1),FTextLocation(CursorLocation, 1));
|
|
Text->DeleteSelectedText();
|
|
Text->GoTo(FTextLocation(CursorLocation, -1));
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
else if (Character == TEXT('\t'))
|
|
{
|
|
SMultiLineEditableText::FScopedEditableTextTransaction Transaction(Text);
|
|
|
|
const FTextLocation CursorLocation = Text->GetCursorLocation();
|
|
|
|
const bool bShouldIncreaseIndentation = InCharacterEvent.GetModifierKeys().IsShiftDown() ? false : true;
|
|
|
|
// When there is no text selected, shift tab should also decrease line indentation
|
|
const bool bShouldIndentLine = Text->AnyTextSelected() || (!Text->AnyTextSelected() && !bShouldIncreaseIndentation);
|
|
|
|
if (bShouldIndentLine)
|
|
{
|
|
// Indent the whole line if there is a text selection
|
|
const FTextSelection Selection = Text->GetSelection();
|
|
const FTextLocation SelectionStart =
|
|
CursorLocation == Selection.GetBeginning() ? Selection.GetEnd() : Selection.GetBeginning();
|
|
|
|
// Shift the selection according to the new indentation
|
|
FTextLocation NewCursorLocation;
|
|
FTextLocation NewSelectionStart;
|
|
|
|
const int32 StartLine = Selection.GetBeginning().GetLineIndex();
|
|
const int32 EndLine = Selection.GetEnd().GetLineIndex();
|
|
|
|
for (int32 Index = StartLine; Index <= EndLine; Index++)
|
|
{
|
|
const FTextLocation LineStart(Index, 0);
|
|
Text->GoTo(LineStart);
|
|
|
|
FString Line;
|
|
Text->GetTextLine(Index, Line);
|
|
const int32 NumSpaces = GetNumSpacesAtStartOfLine(Line);
|
|
const int32 NumExtraSpaces = NumSpaces % 4;
|
|
|
|
// Tab to nearest 4.
|
|
int32 NumSpacesForIndentation;
|
|
if (bShouldIncreaseIndentation)
|
|
{
|
|
NumSpacesForIndentation = NumExtraSpaces == 0 ? 4 : 4 - NumExtraSpaces ;
|
|
Text->InsertTextAtCursor(FString::ChrN(NumSpacesForIndentation, TEXT(' ')));
|
|
}
|
|
else
|
|
{
|
|
NumSpacesForIndentation = NumExtraSpaces == 0 ? FMath::Min(4, NumSpaces) : NumExtraSpaces;
|
|
Text->SelectText(LineStart,FTextLocation(LineStart, NumSpacesForIndentation));
|
|
Text->DeleteSelectedText();
|
|
}
|
|
|
|
const int32 CursorShiftDirection = bShouldIncreaseIndentation ? 1 : -1;
|
|
const int32 CursorShift = NumSpacesForIndentation * CursorShiftDirection;
|
|
|
|
if (Index == CursorLocation.GetLineIndex())
|
|
{
|
|
NewCursorLocation = FTextLocation(CursorLocation, CursorShift);
|
|
}
|
|
|
|
if (Index == SelectionStart.GetLineIndex())
|
|
{
|
|
NewSelectionStart = FTextLocation(SelectionStart, CursorShift);
|
|
}
|
|
}
|
|
|
|
Text->SelectText(NewSelectionStart, NewCursorLocation);
|
|
}
|
|
else
|
|
{
|
|
FString Line;
|
|
Text->GetCurrentTextLine(Line);
|
|
|
|
const int32 Offset = CursorLocation.GetOffset();
|
|
|
|
// Tab to nearest 4.
|
|
if (ensure(bShouldIncreaseIndentation))
|
|
{
|
|
const int32 NumSpacesForIndentation = 4 - Offset % 4;
|
|
Text->InsertTextAtCursor(FString::ChrN(NumSpacesForIndentation, TEXT(' ')));
|
|
}
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
else if (Character == TEXT('\n') || Character == TEXT('\r'))
|
|
{
|
|
SMultiLineEditableText::FScopedEditableTextTransaction Transaction(Text);
|
|
|
|
// at this point, the text after the text cursor is already in a new line
|
|
HandleAutoIndent();
|
|
|
|
return FReply::Handled();
|
|
}
|
|
else if (IsOpenBrace(Character))
|
|
{
|
|
const TCHAR CloseBrace = GetMatchedCloseBrace(Character);
|
|
const FTextLocation CursorLocation = Text->GetCursorLocation();
|
|
FString Line;
|
|
Text->GetCurrentTextLine(Line);
|
|
|
|
bool bShouldAutoInsertBraces = false;
|
|
|
|
if (CursorLocation.GetOffset() < Line.Len())
|
|
{
|
|
const TCHAR NextChar = Text->GetCharacterAt(CursorLocation);
|
|
|
|
if (IsWhiteSpace(NextChar))
|
|
{
|
|
bShouldAutoInsertBraces = true;
|
|
}
|
|
else if (IsCloseBrace(NextChar))
|
|
{
|
|
int32 BraceBalancePrior = 0;
|
|
for (int32 Index = 0; Index < CursorLocation.GetOffset() && Index < Line.Len(); Index++)
|
|
{
|
|
BraceBalancePrior += (Line[Index] == Character);
|
|
BraceBalancePrior -= (Line[Index] == CloseBrace);
|
|
}
|
|
|
|
int32 BraceBalanceLater = 0;
|
|
for (int32 Index = CursorLocation.GetOffset(); Index < Line.Len(); Index++)
|
|
{
|
|
BraceBalanceLater += (Line[Index] == Character);
|
|
BraceBalanceLater -= (Line[Index] == CloseBrace);
|
|
}
|
|
|
|
if (BraceBalancePrior >= -BraceBalanceLater)
|
|
{
|
|
bShouldAutoInsertBraces = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bShouldAutoInsertBraces = true;
|
|
}
|
|
|
|
// auto insert if we have more open braces
|
|
// on the left side than close braces on the right side
|
|
if (bShouldAutoInsertBraces)
|
|
{
|
|
// auto insert the matched close brace
|
|
SMultiLineEditableText::FScopedEditableTextTransaction Transaction(Text);
|
|
Text->InsertTextAtCursor(FString::Chr(Character));
|
|
Text->InsertTextAtCursor(FString::Chr(CloseBrace));
|
|
const FTextLocation NewCursorLocation(Text->GetCursorLocation(), -1);
|
|
Text->GoTo(NewCursorLocation);
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
else if (IsCloseBrace(Character))
|
|
{
|
|
if (!Text->AnyTextSelected())
|
|
{
|
|
const FTextLocation CursorLocation = Text->GetCursorLocation();
|
|
FString Line;
|
|
Text->GetTextLine(CursorLocation.GetLineIndex(), Line);
|
|
|
|
const int32 Offset =CursorLocation.GetOffset();
|
|
|
|
if (Line.IsValidIndex(Offset))
|
|
{
|
|
if (Line[Offset] == Character)
|
|
{
|
|
// avoid creating a duplicated close brace and simply
|
|
// advance the cursor
|
|
Text->GoTo(FTextLocation(CursorLocation, 1));
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
else
|
|
{
|
|
// Let SMultiLineEditableText::OnKeyChar handle it.
|
|
return FReply::Unhandled();
|
|
}
|
|
}
|
|
|
|
void SPCGNodeSourceTextBox::HandleAutoIndent() const
|
|
{
|
|
const FTextLocation CursorLocation = Text->GetCursorLocation();
|
|
const int32 CurLineIndex = CursorLocation.GetLineIndex();
|
|
const int32 LastLineIndex = CurLineIndex - 1;
|
|
|
|
if (LastLineIndex > 0)
|
|
{
|
|
FString LastLine;
|
|
Text->GetTextLine(LastLineIndex, LastLine);
|
|
|
|
const int32 NumSpaces = GetNumSpacesAtStartOfLine(LastLine);
|
|
|
|
const int32 NumSpacesForCurrentIndentation = NumSpaces/4*4;
|
|
const FString CurrentIndentation = FString::ChrN(NumSpacesForCurrentIndentation, TEXT(' '));
|
|
const FString NextIndentation = FString::ChrN(NumSpacesForCurrentIndentation + 4, TEXT(' '));
|
|
|
|
// See what the open/close curly brace balance is.
|
|
int32 BraceBalance = 0;
|
|
for (const TCHAR Char : LastLine)
|
|
{
|
|
BraceBalance += (Char == TEXT('{'));
|
|
BraceBalance -= (Char == TEXT('}'));
|
|
}
|
|
|
|
if (BraceBalance <= 0)
|
|
{
|
|
Text->InsertTextAtCursor(CurrentIndentation);
|
|
}
|
|
else
|
|
{
|
|
Text->InsertTextAtCursor(NextIndentation);
|
|
|
|
// Look for an extra close curly brace and auto-indent it as well
|
|
FString CurLine;
|
|
Text->GetTextLine(CurLineIndex, CurLine);
|
|
|
|
BraceBalance = 0;
|
|
int32 CloseBraceOffset = 0;
|
|
for (const TCHAR Char : CurLine)
|
|
{
|
|
BraceBalance += (Char == TEXT('{'));
|
|
BraceBalance -= (Char == TEXT('}'));
|
|
|
|
// Found the first extra '}'
|
|
if (BraceBalance < 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
CloseBraceOffset++;
|
|
}
|
|
|
|
if (BraceBalance < 0)
|
|
{
|
|
const FTextLocation SavedCursorLocation = Text->GetCursorLocation();
|
|
const FTextLocation CloseBraceLocation(CurLineIndex, CloseBraceOffset);
|
|
|
|
// Create a new line and apply indentation for the close curly brace
|
|
FString NewLineAndIndent(TEXT("\n"));
|
|
NewLineAndIndent.Append(CurrentIndentation);
|
|
|
|
Text->GoTo(CloseBraceLocation);
|
|
Text->InsertTextAtCursor(NewLineAndIndent);
|
|
// Recover cursor location
|
|
Text->GoTo(SavedCursorLocation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SPCGNodeSourceTextBox::IsReadOnly() const
|
|
{
|
|
if (IPCGNodeSourceTextProvider* Provider = Cast<IPCGNodeSourceTextProvider>(ShaderTextProviderObject))
|
|
{
|
|
return Provider->IsShaderTextReadOnly();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|