Files
UnrealEngine/Engine/Source/Editor/InputBindingEditor/Private/InputBindingEditorModule.cpp
2025-05-18 13:04:45 +08:00

519 lines
16 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Containers/Array.h"
#include "Containers/Map.h"
#include "Containers/UnrealString.h"
#include "CoreGlobals.h"
#include "Delegates/Delegate.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "Editor.h"
#include "Editor/EditorEngine.h"
#include "EditorKeyboardShortcutSettings.h"
#include "Fonts/SlateFontInfo.h"
#include "Framework/Commands/InputBindingManager.h"
#include "Framework/Commands/UICommandInfo.h"
#include "HAL/FileManager.h"
#include "HAL/IConsoleManager.h"
#include "HAL/Platform.h"
#include "HAL/PlatformCrt.h"
#include "HAL/PlatformMisc.h"
#include "IDetailCustomization.h"
#include "IDetailGroup.h"
#include "ISettingsModule.h"
#include "ISettingsSection.h"
#include "Interfaces/IInputBindingEditorModule.h"
#include "Internationalization/Internationalization.h"
#include "Internationalization/Text.h"
#include "Layout/Margin.h"
#include "Logging/MessageLog.h"
#include "Math/Color.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Attribute.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/MessageDialog.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorDelegates.h"
#include "PropertyEditorModule.h"
#include "SlotBase.h"
#include "Styling/SlateColor.h"
#include "Templates/SharedPointer.h"
#include "TimerManager.h"
#include "Types/SlateEnums.h"
#include "UObject/Class.h"
#include "UObject/NameTypes.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/UnrealNames.h"
#include "UObject/WeakObjectPtr.h"
#include "UnrealEdMisc.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SChordEditBox.h"
#include "Widgets/Text/STextBlock.h"
class SWidget;
#define LOCTEXT_NAMESPACE "InputBindingEditor"
namespace InputBindingEditorModule
{
static const FName SettingsModuleName("Settings");
static const FName PropertyEditorModuleName("PropertyEditor");
bool bShowBindingNames = false;
static FAutoConsoleVariableRef CVarDebugBindingNames(TEXT("Input.Debug.ShowBindingNames"), bShowBindingNames, TEXT("True to show binding names in the input binding editor."));
}
/**
* A gesture sort functor. Sorts by name or gesture and ascending or descending
*/
struct FChordSort
{
FChordSort( bool bInSortName, bool bInSortUp )
: bSortName( bInSortName )
, bSortUp( bInSortUp )
{ }
bool operator()( const TSharedPtr<FUICommandInfo>& A, const TSharedPtr<FUICommandInfo>& B ) const
{
if( bSortName )
{
// Sort by command bundle, and then by command label. If a command has no bundle,
// it will compare its label to the other command's bundle.
const int32 CompareResult = GetPrimaryTextForCommand(A).CompareTo(GetPrimaryTextForCommand(B));
bool bFinalResult = CompareResult < 0;
if (CompareResult == 0)
{
bFinalResult = A->GetLabel().CompareTo(B->GetLabel()) < 0;
}
return bSortUp ? !bFinalResult : bFinalResult;
}
else
{
// Sort by binding
bool bResult = A->GetInputText().CompareTo( B->GetInputText() ) < 0;
return bSortUp ? !bResult : bResult;
}
}
private:
/** Helper function to check if command is in a bundle, and if so, return the bundle description */
const FText& GetPrimaryTextForCommand(const TSharedPtr<FUICommandInfo>& Command) const
{
if (Command->GetBundle() != NAME_None)
{
TSharedPtr<FBindingContext> Context = FInputBindingManager::Get().GetContextByName(Command->GetBindingContext());
return Context->GetBundleLabel(Command->GetBundle());
}
else
{
return Command->GetLabel();
}
}
/** Whether or not to sort by name. If false we sort by binding. */
bool bSortName;
/** Whether or not to sort up. If false we sort down. */
bool bSortUp;
};
/**
* An item for the chord tree view
*/
struct FChordTreeItem
{
// Note these are mutually exclusive
TWeakPtr<FBindingContext> BindingContext;
TSharedPtr<FUICommandInfo> CommandInfo;
TSharedPtr<FBindingContext> GetBindingContext() { return BindingContext.Pin(); }
bool IsContext() const { return BindingContext.IsValid(); }
bool IsCommand() const { return CommandInfo.IsValid(); }
};
class FEditorKeyboardShortcutSettings : public IDetailCustomization
{
public:
FEditorKeyboardShortcutSettings() :
bUpdateRequested(false),
DetailBuilder(nullptr)
{
}
virtual ~FEditorKeyboardShortcutSettings()
{
FBindingContext::CommandsChanged.RemoveAll(this);
FInputBindingManager::Get().SaveInputBindings();
}
static TSharedRef<IDetailCustomization> MakeInstance()
{
return MakeShareable( new FEditorKeyboardShortcutSettings );
}
virtual void CustomizeDetails(IDetailLayoutBuilder& InDetailBuilder) override
{
DetailBuilder = &InDetailBuilder;
UpdateContextList();
UpdateUI();
FBindingContext::CommandsChanged.AddSP( SharedThis( this ), &FEditorKeyboardShortcutSettings::OnCommandsChanged );
}
/** Updates the context list with new commands. */
void UpdateContextList()
{
FInputBindingManager& InputBindingManager = FInputBindingManager::Get();
TArray< TSharedPtr<FBindingContext> > Contexts;
InputBindingManager.GetKnownInputContexts( Contexts );
// Filter to allowed bindings
Contexts.RemoveAll([&InputBindingManager](const TSharedPtr<FBindingContext>& Context)
{
return !InputBindingManager.CommandPassesFilter(FName(), Context->GetContextName());
});
Contexts.Sort([](const TSharedPtr<FBindingContext>& A, const TSharedPtr<FBindingContext>& B)
{
return A->GetContextDesc().CompareTo(B->GetContextDesc()) < 0;
});
/** List of all known contexts. */
ContextList.Reset(Contexts.Num());
for (const TSharedPtr<FBindingContext>& Context : Contexts)
{
TSharedRef<FChordTreeItem> TreeItem( new FChordTreeItem );
TreeItem->BindingContext = Context;
ContextList.Add( TreeItem );
}
}
void ForceRefreshDetails()
{
bUpdateRequested = false;
UpdateContextList();
if (DetailBuilder)
{
FBindingContext::CommandsChanged.RemoveAll(this);
IDetailLayoutBuilder* DetailBuilderPtr = DetailBuilder;
DetailBuilder = nullptr;
DetailBuilderPtr->ForceRefreshDetails();
}
}
void OnCommandsChanged(const FBindingContext& ContextThatChanged)
{
if (!bUpdateRequested)
{
bUpdateRequested = true;
GEditor->GetTimerManager()->SetTimerForNextTick(FTimerDelegate::CreateSP(this, &FEditorKeyboardShortcutSettings::ForceRefreshDetails));
}
}
void UpdateUI()
{
FInputBindingManager& InputBindingManager = FInputBindingManager::Get();
for (TSharedPtr<FChordTreeItem>& TreeItem : ContextList)
{
check(TreeItem->IsContext());
IDetailCategoryBuilder& CategoryBuilder = DetailBuilder->EditCategory(TreeItem->GetBindingContext()->GetContextName(), TreeItem->GetBindingContext()->GetContextDesc());
TArray<TSharedPtr<FUICommandInfo>> Commands;
InputBindingManager.GetCommandInfosFromContext(TreeItem->GetBindingContext()->GetContextName(), Commands);
// Filter to allowed bindings
Commands.RemoveAll([&InputBindingManager](const TSharedPtr<FUICommandInfo>& CommandInfo)
{
return !InputBindingManager.CommandPassesFilter(CommandInfo->GetBindingContext(), CommandInfo->GetCommandName());
});
Commands.Sort(FChordSort(true, false));
TMap<FName, IDetailGroup*> BundleMap;
for(TSharedPtr<FUICommandInfo>& CommandInfo : Commands)
{
FDetailWidgetRow* Row = nullptr;
const FName BundleName = CommandInfo->GetBundle();
if (!BundleName.IsNone())
{
if (!BundleMap.Contains(BundleName))
{
// TreeItem is guaranteed to have BindingContext due to check() above
const FText& BundleLabel = TreeItem->GetBindingContext()->GetBundleLabel(BundleName);
IDetailGroup* Group = BundleMap.Add(BundleName, &CategoryBuilder.AddGroup(BundleName, BundleLabel));
// Match this widget with the "Label" widget on non-bundled commands (see below)
Group->HeaderRow().NameContent()
.MaxDesiredWidth(0)
.MinDesiredWidth(500)
[
SNew(SBox)
.Padding(FMargin(0.0f, 3.0f, 0.0f, 3.0f))
[
SNew(STextBlock)
.Text(BundleLabel)
]
];
}
Row = &BundleMap[BundleName]->AddWidgetRow();
Row->FilterString(CommandInfo->GetLabel());
}
else
{
Row = &CategoryBuilder.AddCustomRow(CommandInfo->GetLabel());
}
// Set up search filter (for i.e. KeyBinding="F")
const TSharedRef<const FInputChord> FirstInputChord = CommandInfo->GetActiveChord(EMultipleKeyBindingIndex::Primary);
const TSharedRef<const FInputChord> SecondInputChord = CommandInfo->GetActiveChord(EMultipleKeyBindingIndex::Secondary);
const bool bFirstChordValid = FirstInputChord->IsValidChord();
const bool bSecondChordValid = SecondInputChord->IsValidChord();
if (bFirstChordValid || bSecondChordValid)
{
static const FTextFormat KeyFormat = LOCTEXT("SearchKeyFilter", "KeyBinding=\"{0}\"");
static const FTextFormat TokenCombineFormat = INVTEXT("{0} {1}");
FText FirstKeyFilter, SecondKeyFilter;
if (bFirstChordValid)
{
FirstKeyFilter = FText::FormatOrdered(KeyFormat, FirstInputChord->GetInputText());
}
if (bSecondChordValid)
{
SecondKeyFilter = FText::FormatOrdered(KeyFormat, FirstInputChord->GetInputText());
}
FText FilterText = bFirstChordValid && bSecondChordValid
? FText::FormatOrdered(TokenCombineFormat, FirstKeyFilter, SecondKeyFilter)
: (bFirstChordValid ? FirstKeyFilter : SecondKeyFilter);
// Command label and similar string might already be stored
if (!Row->FilterTextString.IsEmpty())
{
FilterText = FText::FormatOrdered(TokenCombineFormat, Row->FilterTextString, FilterText);
}
Row->FilterString(FilterText);
}
Row->NameContent()
.MaxDesiredWidth(0)
.MinDesiredWidth(500)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(STextBlock)
.Text(CommandInfo->GetLabel())
.ToolTipText_Lambda([CommandInfo]() -> FText
{
FText CommandInfoTooltip = CommandInfo->GetDescription();
if (InputBindingEditorModule::bShowBindingNames)
{
CommandInfoTooltip = FText::Format(LOCTEXT("CommandInfoDebugToolTip", "{0}\n\nBinding Context: {1}\nCommand Name: {2}"), CommandInfoTooltip, FText::FromName(CommandInfo->GetBindingContext()), FText::FromName(CommandInfo->GetCommandName()));
}
return CommandInfoTooltip;
})
]
+ SVerticalBox::Slot()
.Padding(0.0f, 3.0f, 0.0f, 3.0f)
.AutoHeight()
[
SNew(STextBlock)
.Font(IDetailLayoutBuilder::GetDetailFont())
.ColorAndOpacity(FLinearColor::Gray)
.Text(CommandInfo->GetDescription())
]
];
Row->ValueContent()
.MaxDesiredWidth(200)
.MinDesiredWidth(200)
.VAlign(VAlign_Center)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(1.0f, 0.0f, 9.0f, 0.0f)
[
SNew(SChordEditBox, CommandInfo, EMultipleKeyBindingIndex::Primary)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SChordEditBox, CommandInfo, EMultipleKeyBindingIndex::Secondary)
]
];
}
}
}
private:
bool bUpdateRequested;
IDetailLayoutBuilder* DetailBuilder;
/** List of all known contexts. */
TArray< TSharedPtr<FChordTreeItem> > ContextList;
};
class FInputBindingEditorModule
: public IInputBindingEditorModule
{
public:
// IInputBindingEditorModule interface
virtual void StartupModule() override
{
ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked<ISettingsModule>(InputBindingEditorModule::SettingsModuleName);
FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(InputBindingEditorModule::PropertyEditorModuleName);
EditorKeyboardShortcutSettingsName = UEditorKeyboardShortcutSettings::StaticClass()->GetFName();
PropertyEditor.RegisterCustomClassLayout(EditorKeyboardShortcutSettingsName, FOnGetDetailCustomizationInstance::CreateStatic(&FEditorKeyboardShortcutSettings::MakeInstance));
// input bindings
ISettingsSectionPtr InputBindingSettingsSection = SettingsModule.RegisterSettings("Editor", "General", "InputBindings",
LOCTEXT("InputBindingsSettingsName", "Keyboard Shortcuts"),
LOCTEXT("InputBindingsSettingsDescription", "Configure keyboard shortcuts to quickly invoke operations."),
GetMutableDefault<UEditorKeyboardShortcutSettings>()
);
if(InputBindingSettingsSection.IsValid())
{
InputBindingSettingsSection->OnExport().BindRaw(this, &FInputBindingEditorModule::HandleInputBindingsExport);
InputBindingSettingsSection->OnImport().BindRaw(this, &FInputBindingEditorModule::HandleInputBindingsImport);
InputBindingSettingsSection->OnResetDefaults().BindRaw(this, &FInputBindingEditorModule::HandleInputBindingsResetToDefault);
InputBindingSettingsSection->OnSave().BindRaw(this, &FInputBindingEditorModule::HandleInputBindingsSave);
}
}
virtual void ShutdownModule() override
{
if(FModuleManager::Get().IsModuleLoaded(InputBindingEditorModule::PropertyEditorModuleName))
{
FPropertyEditorModule& PropertyEditor = FModuleManager::GetModuleChecked<FPropertyEditorModule>(InputBindingEditorModule::PropertyEditorModuleName);
PropertyEditor.UnregisterCustomClassLayout(EditorKeyboardShortcutSettingsName);
}
}
private:
// Show a warning that the editor will require a restart and return its result
EAppReturnType::Type ShowRestartWarning(const FText& Title) const
{
return FMessageDialog::Open(EAppMsgType::OkCancel, LOCTEXT("ActionRestartMsg", "Imported settings won't be applied until the editor is restarted. Do you wish to restart now (you will be prompted to save any changes)?"), Title);
}
// Backup a file
bool BackupFile(const FString& SrcFilename, const FString& DstFilename)
{
if(IFileManager::Get().Copy(*DstFilename, *SrcFilename) == COPY_OK)
{
return true;
}
// log error
FMessageLog EditorErrors("EditorErrors");
if(!FPaths::FileExists(SrcFilename))
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("FileName"), FText::FromString(SrcFilename));
EditorErrors.Warning(FText::Format(LOCTEXT("UnsuccessfulBackup_NoExist_Notification", "Unsuccessful backup! {FileName} does not exist!"), Arguments));
}
else if(IFileManager::Get().IsReadOnly(*DstFilename))
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("FileName"), FText::FromString(DstFilename));
EditorErrors.Warning(FText::Format(LOCTEXT("UnsuccessfulBackup_ReadOnly_Notification", "Unsuccessful backup! {FileName} is read-only!"), Arguments));
}
else
{
FFormatNamedArguments Arguments;
Arguments.Add(TEXT("SourceFileName"), FText::FromString(SrcFilename));
Arguments.Add(TEXT("BackupFileName"), FText::FromString(DstFilename));
// We don't specifically know why it failed, this is a fallback.
EditorErrors.Warning(FText::Format(LOCTEXT("UnsuccessfulBackup_Fallback_Notification", "Unsuccessful backup of {SourceFileName} to {BackupFileName}"), Arguments));
}
EditorErrors.Notify(LOCTEXT("BackupUnsuccessful_Title", "Backup Unsuccessful!"));
return false;
}
// Handles exporting input bindings to a file
bool HandleInputBindingsExport(const FString& Filename)
{
FInputBindingManager::Get().SaveInputBindings();
GConfig->Flush(false, GEditorKeyBindingsIni);
return BackupFile(GEditorKeyBindingsIni, Filename);
}
// Handles importing input bindings from a file
bool HandleInputBindingsImport(const FString& Filename)
{
if(EAppReturnType::Ok == ShowRestartWarning(LOCTEXT("ImportKeyBindings_Title", "Import Key Bindings")))
{
FUnrealEdMisc::Get().SetConfigRestoreFilename(Filename, GEditorKeyBindingsIni);
FUnrealEdMisc::Get().RestartEditor(false);
return true;
}
return false;
}
// Handles resetting input bindings back to the defaults
bool HandleInputBindingsResetToDefault()
{
if(EAppReturnType::Ok == ShowRestartWarning(LOCTEXT("ResetKeyBindings_Title", "Reset Key Bindings")))
{
FInputBindingManager::Get().RemoveUserDefinedChords();
GConfig->Flush(false, GEditorKeyBindingsIni);
FUnrealEdMisc::Get().RestartEditor(false);
return true;
}
return false;
}
// Handles saving default input bindings.
bool HandleInputBindingsSave()
{
FInputBindingManager::Get().RemoveUserDefinedChords();
GConfig->Flush(false, GEditorKeyBindingsIni);
return true;
}
private:
/** Holds the collection of created binding editor panels. */
TArray<TSharedPtr<SWidget> > BindingEditorPanels;
/** Captured name of the UEditorKeyboardShortcutSettings class */
FName EditorKeyboardShortcutSettingsName;
};
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FInputBindingEditorModule, InputBindingEditor);