diff --git a/Source/LifePlay/LPKeyboardSubsystem.cpp b/Source/LifePlay/LPKeyboardSubsystem.cpp new file mode 100644 index 0000000..221a503 --- /dev/null +++ b/Source/LifePlay/LPKeyboardSubsystem.cpp @@ -0,0 +1,958 @@ +// Menu Clavier. Voir LPKeyboardSubsystem.h. + +#include "LPKeyboardSubsystem.h" + +#include "Components/Button.h" +#include "Components/ScrollBox.h" +#include "Components/ScrollBoxSlot.h" +#include "Components/TextBlock.h" +#include "Engine/GameInstance.h" +#include "Engine/GameViewportClient.h" +#include "Engine/World.h" +#include "Framework/Application/IInputProcessor.h" +#include "Framework/Application/SlateApplication.h" +#include "GameFramework/InputSettings.h" +#include "GameFramework/PlayerController.h" +#include "GameFramework/PlayerInput.h" +#include "HAL/FileManager.h" +#include "LifePlay.h" +#include "Misc/ConfigCacheIni.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Styling/CoreStyle.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" + +namespace +{ + const FName ButtonName(TEXT("LPKeyboardRow")); + const TCHAR* InputSection = TEXT("/Script/Engine.InputSettings"); + + FString KeyboardIniPath() + { + return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config/Windows/LPKeyboard.ini")); + } + + FString ExportAction(const FInputActionKeyMapping& Map) + { + FString Text; + FInputActionKeyMapping::StaticStruct()->ExportText(Text, &Map, nullptr, nullptr, PPF_None, nullptr); + return Text; + } + + FString ExportAxis(const FInputAxisKeyMapping& Map) + { + FString Text; + FInputAxisKeyMapping::StaticStruct()->ExportText(Text, &Map, nullptr, nullptr, PPF_None, nullptr); + return Text; + } + + struct FBindRow + { + FName Name; + FString Label; + bool bAxis = false; + float Scale = 0.f; + FKey Key; + bool bShift = false; + bool bCtrl = false; + bool bAlt = false; + bool bCmd = false; + }; + + struct FLiveKeyboard + { + TWeakObjectPtr Owner; + TSharedPtr Menu; + TSharedPtr Processor; + bool bEmbedded = false; + }; + + bool IsRemappable(const FKey& Key) + { + return Key.IsValid() && !Key.IsAnalog() && !Key.IsAxis1D() && !Key.IsGamepadKey(); + } + + FString ActionLabel(FName Name) + { + static const TCHAR* Known[][2] = { + {TEXT("Pause"), TEXT("Pause")}, + {TEXT("Journal"), TEXT("Journal")}, + {TEXT("Inventory"), TEXT("Inventory")}, + {TEXT("MainMenu"), TEXT("Main Menu")}, + {TEXT("CharacterMenu"), TEXT("Character Menu")}, + {TEXT("ModuleMenu"), TEXT("Modules")}, + {TEXT("CityMenu"), TEXT("City")}, + {TEXT("StatMenu"), TEXT("Statistics")}, + {TEXT("ContactsMenu"), TEXT("Contacts")}, + {TEXT("RotateCamera"), TEXT("Rotate Camera")}, + {TEXT("ManualAnimating"), TEXT("Manual Animation")}, + {TEXT("SpeedUp"), TEXT("Speed Up")}, + {TEXT("SpeedDown"), TEXT("Slow Down")}, + {TEXT("SwapRoles"), TEXT("Swap Roles")}, + {TEXT("SwitchCamera"), TEXT("Switch Camera")}, + {TEXT("NextBuilding"), TEXT("Next Building")}, + {TEXT("PreviousBuilding"), TEXT("Previous Building")}, + {TEXT("TestScenes"), TEXT("Test Scenes")}, + {TEXT("Recommended"), TEXT("Recommended")}, + {TEXT("QuickSave"), TEXT("Quick Save")}, + {TEXT("Quickload"), TEXT("Quick Load")}, + {TEXT("SearchBarToggle"), TEXT("Search")}, + {TEXT("Interact"), TEXT("Interact")}, + }; + const FString Raw = Name.ToString(); + for (int32 Index = 0; Index < UE_ARRAY_COUNT(Known); ++Index) + { + if (Raw == Known[Index][0]) + { + return Known[Index][1]; + } + } + if (Raw.StartsWith(TEXT("Favorite"))) + { + return TEXT("Favorite ") + Raw.RightChop(8); + } + return Raw; + } + + FString AxisLabel(FName Name, float Scale) + { + const bool bPositive = Scale >= 0.f; + if (Name == TEXT("LeftRight")) + { + return bPositive ? TEXT("Right") : TEXT("Left"); + } + if (Name == TEXT("DownUp")) + { + return bPositive ? TEXT("Forward") : TEXT("Back"); + } + if (Name == TEXT("MoveForward")) + { + return bPositive ? TEXT("Walk Forward") : TEXT("Walk Back"); + } + if (Name == TEXT("MoveRight")) + { + return bPositive ? TEXT("Walk Right") : TEXT("Walk Left"); + } + if (Name == TEXT("PenisRotation")) + { + return bPositive ? TEXT("Rotation +") : TEXT("Rotation −"); + } + return Name.ToString() + (bPositive ? TEXT(" +") : TEXT(" −")); + } + + struct FBindGroup + { + int32 Order = 9; + const TCHAR* Title = TEXT("Other"); + }; + + FBindGroup GroupFor(FName Name) + { + const FString Raw = Name.ToString(); + if (Raw.StartsWith(TEXT("Favorite"))) + { + return {4, TEXT("Favorites")}; + } + if (Name == TEXT("MoveForward") || Name == TEXT("MoveRight")) + { + return {0, TEXT("Movement")}; + } + if (Name == TEXT("LeftRight") || Name == TEXT("DownUp") || Name == TEXT("ScrollDownUp") + || Name == TEXT("RotateCamera") || Name == TEXT("SwitchCamera")) + { + return {1, TEXT("Camera")}; + } + if (Name == TEXT("Interact") || Name == TEXT("SwapRoles")) + { + return {2, TEXT("Interaction")}; + } + if (Name == TEXT("Pause") || Name == TEXT("Journal") || Name == TEXT("Inventory") + || Name == TEXT("MainMenu") || Name == TEXT("CharacterMenu") || Name == TEXT("ModuleMenu") + || Name == TEXT("CityMenu") || Name == TEXT("StatMenu") || Name == TEXT("ContactsMenu") + || Name == TEXT("SearchBarToggle")) + { + return {3, TEXT("Menus")}; + } + if (Name == TEXT("SpeedUp") || Name == TEXT("SpeedDown")) + { + return {5, TEXT("Time")}; + } + if (Name == TEXT("NextBuilding") || Name == TEXT("PreviousBuilding")) + { + return {6, TEXT("World")}; + } + if (Name == TEXT("QuickSave") || Name == TEXT("Quickload")) + { + return {7, TEXT("Save")}; + } + if (Name == TEXT("ManualAnimating") || Name == TEXT("PenisRotation") || Name == TEXT("TestScenes") || Name == TEXT("Recommended")) + { + return {8, TEXT("Scene")}; + } + return {}; + } + + FString ChordText(const FKey& Key, bool bShift, bool bCtrl, bool bAlt) + { + FString Text; + if (bCtrl) + { + Text += TEXT("Ctrl+"); + } + if (bShift) + { + Text += TEXT("Shift+"); + } + if (bAlt) + { + Text += TEXT("Alt+"); + } + Text += Key.GetDisplayName().ToString(); + return Text; + } + + TSharedRef MakeText(const FString& Value, int32 Size, const FLinearColor& Color) + { + FSlateFontInfo Font = FCoreStyle::GetDefaultFontStyle("Regular", Size); + Font.OutlineSettings.OutlineSize = 2; + Font.OutlineSettings.bApplyOutlineToDropShadows = true; + Font.OutlineSettings.OutlineColor = FLinearColor(0.f, 0.f, 0.f, 0.8f); + return SNew(STextBlock) + .Text(FText::FromString(Value)) + .Font(Font) + .ColorAndOpacity(FSlateColor(Color)); + } + + class SLPKeyboardMenu : public SCompoundWidget + { + public: + SLATE_BEGIN_ARGS(SLPKeyboardMenu) {} + SLATE_ARGUMENT(bool, bEmbedded) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, ULPKeyboardSubsystem* InOwner) + { + Owner = InOwner; + bEmbedded = InArgs._bEmbedded; + ChildSlot + [ + SNew(SBox) + .HAlign(bEmbedded ? HAlign_Fill : HAlign_Center) + .VAlign(bEmbedded ? VAlign_Fill : VAlign_Center) + [ + SNew(SBorder) + .BorderImage(bEmbedded ? FCoreStyle::Get().GetBrush("NoBrush") : FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder")) + .Padding(bEmbedded ? 0.f : 16.f) + [ + SNew(SBox) + .WidthOverride(bEmbedded ? FOptionalSize() : FOptionalSize(560.f)) + .HeightOverride(bEmbedded ? FOptionalSize() : FOptionalSize(640.f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Change a key, add one, or remove one. Escape cancels the capture. Saved for this version of the game."))) + .AutoWrapText(true) + .Font(MakeText(TEXT(""), 16, FLinearColor::White)->GetFont()) + .ColorAndOpacity(FSlateColor(FLinearColor(1.f, 1.f, 1.f, 0.95f))) + ] + + SVerticalBox::Slot().FillHeight(1.f) + [ + SNew(SScrollBox) + + SScrollBox::Slot() + [ + SAssignNew(Rows, SVerticalBox) + ] + ] + + SVerticalBox::Slot().AutoHeight().Padding(0.f, 8.f, 0.f, 8.f) + [ + SAssignNew(StatusBlock, STextBlock) + .Font(MakeText(TEXT(""), 16, FLinearColor::White)->GetFont()) + .ColorAndOpacity(FSlateColor(FLinearColor(0.95f, 0.85f, 0.4f))) + ] + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.f).Padding(0.f, 0.f, 8.f, 0.f) + [ + SNew(SButton) + .OnClicked(this, &SLPKeyboardMenu::OnReset) + [ + MakeText(TEXT("Reset"), 20, FLinearColor::White) + ] + ] + + SHorizontalBox::Slot().FillWidth(1.f) + [ + SNew(SButton) + .Visibility(bEmbedded ? EVisibility::Collapsed : EVisibility::Visible) + .OnClicked(this, &SLPKeyboardMenu::OnClose) + [ + MakeText(TEXT("Close"), 20, FLinearColor::White) + ] + ] + ] + ] + ] + ] + ]; + Rebuild(); + } + + void Rebuild() + { + if (!Rows.IsValid()) + { + return; + } + Rows->ClearChildren(); + ULPKeyboardSubsystem* Sub = Owner.Get(); + const UInputSettings* Settings = UInputSettings::GetInputSettings(); + if (!Sub || !Settings) + { + return; + } + TArray Binds; + for (const FInputActionKeyMapping& Map : Settings->GetActionMappings()) + { + if (!IsRemappable(Map.Key)) + { + continue; + } + FBindRow Row; + Row.Name = Map.ActionName; + Row.Label = ActionLabel(Map.ActionName); + Row.Key = Map.Key; + Row.bShift = Map.bShift; + Row.bCtrl = Map.bCtrl; + Row.bAlt = Map.bAlt; + Row.bCmd = Map.bCmd; + Binds.Add(Row); + } + for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) + { + if (!IsRemappable(Map.Key)) + { + continue; + } + FBindRow Row; + Row.Name = Map.AxisName; + Row.Label = AxisLabel(Map.AxisName, Map.Scale); + Row.bAxis = true; + Row.Scale = Map.Scale; + Row.Key = Map.Key; + Binds.Add(Row); + } + Binds.Sort([](const FBindRow& A, const FBindRow& B) + { + const int32 GroupA = GroupFor(A.Name).Order; + const int32 GroupB = GroupFor(B.Name).Order; + if (GroupA != GroupB) + { + return GroupA < GroupB; + } + if (A.Label != B.Label) + { + return A.Label < B.Label; + } + return A.Key.GetDisplayName().ToString() < B.Key.GetDisplayName().ToString(); + }); + + int32 PreviousGroup = INDEX_NONE; + FString Previous; + for (const FBindRow& Row : Binds) + { + const FBindGroup Section = GroupFor(Row.Name); + if (Section.Order != PreviousGroup) + { + PreviousGroup = Section.Order; + Previous.Reset(); + Rows->AddSlot().AutoHeight().Padding(0.f, 12.f, 0.f, 4.f) + [ + MakeText(Section.Title, 20, FLinearColor(1.f, 0.92f, 0.45f)) + ]; + } + if (Row.Label != Previous) + { + Previous = Row.Label; + const FBindRow Group = Row; + Rows->AddSlot().AutoHeight().Padding(0.f, 10.f, 0.f, 2.f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center) + [ + MakeText(Row.Label, 20, FLinearColor(1.f, 1.f, 1.f, 0.95f)) + ] + + SHorizontalBox::Slot().AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([this, Group]() + { + if (ULPKeyboardSubsystem* Sub = Owner.Get()) + { + Sub->BeginWait(Group.Name, Group.bAxis, Group.Scale, EKeys::Invalid, false, false, false, false, true); + Sub->RefreshMenu(); + } + return FReply::Handled(); + }) + [ + MakeText(TEXT("Add"), 16, FLinearColor::White) + ] + ] + ]; + } + const FBindRow Current = Row; + Rows->AddSlot().AutoHeight().Padding(12.f, 2.f, 0.f, 2.f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center) + [ + MakeText(ChordText(Current.Key, Current.bShift, Current.bCtrl, Current.bAlt), 16, FLinearColor(0.85f, 0.9f, 1.f)) + ] + + SHorizontalBox::Slot().AutoWidth().Padding(4.f, 0.f) + [ + SNew(SButton) + .OnClicked_Lambda([this, Current]() + { + if (ULPKeyboardSubsystem* Sub = Owner.Get()) + { + Sub->BeginWait(Current.Name, Current.bAxis, Current.Scale, Current.Key, Current.bShift, Current.bCtrl, Current.bAlt, Current.bCmd, false); + Sub->RefreshMenu(); + } + return FReply::Handled(); + }) + [ + MakeText(TEXT("Change"), 16, FLinearColor::White) + ] + ] + + SHorizontalBox::Slot().AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([this, Current]() + { + if (ULPKeyboardSubsystem* Sub = Owner.Get()) + { + Sub->RemoveBinding(Current.Name, Current.bAxis, Current.Scale, Current.Key, Current.bShift, Current.bCtrl, Current.bAlt, Current.bCmd); + } + return FReply::Handled(); + }) + [ + MakeText(TEXT("Remove"), 16, FLinearColor::White) + ] + ] + ]; + } + if (StatusBlock.IsValid() && Sub) + { + StatusBlock->SetText(FText::FromString(Sub->GetStatus())); + } + } + + private: + FReply OnReset() + { + if (ULPKeyboardSubsystem* Sub = Owner.Get()) + { + Sub->ResetToDefault(); + } + return FReply::Handled(); + } + + FReply OnClose() + { + if (ULPKeyboardSubsystem* Sub = Owner.Get()) + { + Sub->CloseKeyboardMenu(); + } + return FReply::Handled(); + } + + TWeakObjectPtr Owner; + TSharedPtr Rows; + TSharedPtr StatusBlock; + bool bEmbedded = false; + }; + + FLiveKeyboard& Live() + { + static FLiveKeyboard State; + return State; + } + + class FKeyboardProcessor : public IInputProcessor + { + public: + virtual void Tick(const float, FSlateApplication&, TSharedRef) override {} + + virtual bool HandleKeyDownEvent(FSlateApplication&, const FKeyEvent& Event) override + { + ULPKeyboardSubsystem* Sub = Live().Owner.Get(); + if (!Sub || !Live().Menu.IsValid()) + { + return false; + } + const FKey Key = Event.GetKey(); + if (Sub->IsWaiting()) + { + if (Key == EKeys::Escape) + { + Sub->CancelWait(); + } + else if (!Key.IsModifierKey()) + { + Sub->CommitKey(Key, Event.IsShiftDown(), Event.IsControlDown(), Event.IsAltDown(), Event.IsCommandDown()); + } + return true; + } + if (Live().bEmbedded) + { + return false; + } + if (Key == EKeys::Escape) + { + Sub->CloseKeyboardMenu(); + return true; + } + return true; + } + }; +} + +bool ULPKeyboardSubsystem::IsMenuOpen() const +{ + return Live().Menu.IsValid() && Live().Owner.Get() == this; +} + +void ULPKeyboardSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + if (!LoadSaved()) + { + SaveSaved(); + } + TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPKeyboardSubsystem::Tick), 0.5f); +} + +void ULPKeyboardSubsystem::Deinitialize() +{ + if (Live().Owner.Get() == this) + { + CloseKeyboardMenu(); + } + FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); + Super::Deinitialize(); +} + +bool ULPKeyboardSubsystem::Tick(float DeltaTime) +{ + (void)DeltaTime; + UGameInstance* GI = GetGameInstance(); + bool bFound = false; + for (TObjectIterator It; It; ++It) + { + UScrollBox* Scroll = *It; + if (!Scroll || Scroll->GetFName() != TEXT("SettingsScrollBox") || Scroll->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) + { + continue; + } + const UUserWidget* Owner = Scroll->GetTypedOuter(); + if (!Owner || Owner->HasAnyFlags(RF_ClassDefaultObject) || Owner->GetGameInstance() != GI) + { + continue; + } + bFound = true; + } + if (!bFound && !bLoggedMissing) + { + bLoggedMissing = true; + UE_LOG(LogLifePlay, Display, TEXT("Clavier : liste des reglages pas encore ouverte")); + } + return true; +} + +void ULPKeyboardSubsystem::AttachButton(UScrollBox* Scroll) +{ + for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) + { + if (Scroll->GetChildAt(Index) && Scroll->GetChildAt(Index)->GetFName() == ButtonName) + { + return; + } + } + UButton* Button = NewObject(Scroll, ButtonName); + UTextBlock* Label = NewObject(Button, TEXT("LPKeyboardLabel")); + Label->SetText(FText::FromString(TEXT("Keyboard"))); + Label->SetColorAndOpacity(FSlateColor(FLinearColor::White)); + FSlateFontInfo Font = FCoreStyle::GetDefaultFontStyle("Regular", 16); + Label->SetFont(Font); + Button->SetContent(Label); + if (UScrollBoxSlot* Slot = Cast(Scroll->AddChild(Button))) + { + Slot->SetPadding(FMargin(0.f, 12.f, 0.f, 4.f)); + Slot->SetHorizontalAlignment(HAlign_Fill); + } + Button->OnClicked.AddDynamic(this, &ULPKeyboardSubsystem::OpenKeyboardMenu); + UE_LOG(LogLifePlay, Display, TEXT("Clavier : bouton ajoute dans les reglages")); +} + +TSharedRef ULPKeyboardSubsystem::TakeEmbeddedPage() +{ + if (Live().Menu.IsValid() && Live().Owner.Get() == this) + { + return Live().Menu.ToSharedRef(); + } + Status = TEXT("Choose Change or Add, then press a key."); + bWaiting = false; + Live().bEmbedded = true; + TSharedRef Panel = SNew(SLPKeyboardMenu, this).bEmbedded(true); + Live().Owner = this; + Live().Menu = Panel; + if (!Live().Processor.IsValid()) + { + Live().Processor = MakeShared(); + FSlateApplication::Get().RegisterInputPreProcessor(Live().Processor.ToSharedRef(), 0); + } + return Panel; +} + +void ULPKeyboardSubsystem::OpenKeyboardMenu() +{ + if (Live().Menu.IsValid() || !FSlateApplication::IsInitialized() || !GEngine || !GEngine->GameViewport) + { + return; + } + Status = TEXT("Choose Change or Add, then press a key."); + bWaiting = false; + Live().bEmbedded = false; + TSharedRef Panel = SNew(SLPKeyboardMenu, this).bEmbedded(false); + Live().Owner = this; + Live().Menu = Panel; + Live().Processor = MakeShared(); + GEngine->GameViewport->AddViewportWidgetContent(Panel, 200); + FSlateApplication::Get().RegisterInputPreProcessor(Live().Processor.ToSharedRef(), 0); +} + +void ULPKeyboardSubsystem::CloseKeyboardMenu() +{ + if (Live().Owner.Get() != this && Live().Owner.IsValid()) + { + return; + } + bWaiting = false; + if (Live().Processor.IsValid() && FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().UnregisterInputPreProcessor(Live().Processor.ToSharedRef()); + } + if (Live().Menu.IsValid() && GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->RemoveViewportWidgetContent(Live().Menu.ToSharedRef()); + } + Live().Menu.Reset(); + Live().Processor.Reset(); + Live().Owner.Reset(); + Live().bEmbedded = false; +} + +void ULPKeyboardSubsystem::BeginWait(FName Name, bool bAxis, float Scale, FKey OldKey, bool bShift, bool bCtrl, bool bAlt, bool bCmd, bool bAdd) +{ + bWaiting = true; + bWaitAdd = bAdd; + bWaitAxis = bAxis; + WaitScale = Scale; + WaitName = Name; + WaitOldKey = OldKey; + bWaitShift = bShift; + bWaitCtrl = bCtrl; + bWaitAlt = bAlt; + bWaitCmd = bCmd; + Status = bAdd + ? FString::Printf(TEXT("New key for %s. Escape cancels."), bAxis ? *AxisLabel(Name, Scale) : *ActionLabel(Name)) + : FString::Printf(TEXT("Replaces %s. Escape cancels."), *OldKey.GetDisplayName().ToString()); +} + +void ULPKeyboardSubsystem::CancelWait() +{ + bWaiting = false; + Status = TEXT("Capture cancelled."); + RefreshMenu(); +} + +bool ULPKeyboardSubsystem::CommitKey(const FKey& Key, bool bShift, bool bCtrl, bool bAlt, bool bCmd) +{ + bWaiting = false; + if (!IsRemappable(Key)) + { + Status = TEXT("This key cannot be assigned."); + RefreshMenu(); + return false; + } + UInputSettings* Settings = UInputSettings::GetInputSettings(); + if (!Settings) + { + return false; + } + TArray Cleared; + if (!bWaitAdd && WaitOldKey.IsValid()) + { + if (bWaitAxis) + { + Settings->RemoveAxisMapping(FInputAxisKeyMapping(WaitName, WaitOldKey, WaitScale), false); + } + else + { + Settings->RemoveActionMapping(FInputActionKeyMapping(WaitName, WaitOldKey, bWaitShift, bWaitCtrl, bWaitAlt, bWaitCmd), false); + } + } + const TArray Actions = Settings->GetActionMappings(); + for (const FInputActionKeyMapping& Map : Actions) + { + if (Map.Key == Key && Map.bShift == bShift && Map.bCtrl == bCtrl && Map.bAlt == bAlt && Map.bCmd == bCmd) + { + Settings->RemoveActionMapping(Map, false); + Cleared.Add(ActionLabel(Map.ActionName)); + } + } + TArray Axes = Settings->GetAxisMappings(); + for (const FInputAxisKeyMapping& Map : Axes) + { + if (Map.Key == Key && !bShift && !bCtrl && !bAlt && !bCmd) + { + Settings->RemoveAxisMapping(Map, false); + Cleared.Add(AxisLabel(Map.AxisName, Map.Scale)); + } + } + if (bWaitAxis) + { + Settings->AddAxisMapping(FInputAxisKeyMapping(WaitName, Key, WaitScale), false); + } + else + { + Settings->AddActionMapping(FInputActionKeyMapping(WaitName, Key, bShift, bCtrl, bAlt, bCmd), false); + } + ApplyToPlayer(); + Status = Cleared.Num() > 0 + ? FString::Printf(TEXT("%s saved. Removed from: %s."), *Key.GetDisplayName().ToString(), *FString::Join(Cleared, TEXT(", "))) + : FString::Printf(TEXT("%s saved."), *Key.GetDisplayName().ToString()); + RefreshMenu(); + return true; +} + +void ULPKeyboardSubsystem::RemoveBinding(FName Name, bool bAxis, float Scale, FKey Key, bool bShift, bool bCtrl, bool bAlt, bool bCmd) +{ + bWaiting = false; + UInputSettings* Settings = UInputSettings::GetInputSettings(); + if (!Settings) + { + return; + } + if (bAxis) + { + Settings->RemoveAxisMapping(FInputAxisKeyMapping(Name, Key, Scale), false); + } + else + { + Settings->RemoveActionMapping(FInputActionKeyMapping(Name, Key, bShift, bCtrl, bAlt, bCmd), false); + } + ApplyToPlayer(); + Status = FString::Printf(TEXT("%s removed."), *Key.GetDisplayName().ToString()); + RefreshMenu(); +} + +bool ULPKeyboardSubsystem::LoadDefaults(TArray& Actions, TArray& Axes) const +{ + const FString Path = FPaths::Combine(FPaths::ProjectConfigDir(), TEXT("DefaultInput.ini")); + TArray Lines; + if (!FFileHelper::LoadFileToStringArray(Lines, *Path)) + { + return false; + } + for (FString Line : Lines) + { + Line.TrimStartAndEndInline(); + if (Line.StartsWith(TEXT("+ActionMappings="))) + { + FInputActionKeyMapping Map; + const FString Inner = Line.Mid(16); + if (FInputActionKeyMapping::StaticStruct()->ImportText(*Inner, &Map, nullptr, PPF_None, nullptr, TEXT("ActionMappings")) && Map.Key.IsValid()) + { + Actions.Add(Map); + } + } + else if (Line.StartsWith(TEXT("+AxisMappings="))) + { + FInputAxisKeyMapping Map; + const FString Inner = Line.Mid(14); + if (FInputAxisKeyMapping::StaticStruct()->ImportText(*Inner, &Map, nullptr, PPF_None, nullptr, TEXT("AxisMappings")) && Map.Key.IsValid()) + { + Axes.Add(Map); + } + } + } + return Actions.Num() > 0 || Axes.Num() > 0; +} + +void ULPKeyboardSubsystem::ResetToDefault() +{ + bWaiting = false; + TArray Actions; + TArray Axes; + UInputSettings* Settings = UInputSettings::GetInputSettings(); + if (!Settings || !LoadDefaults(Actions, Axes)) + { + Status = TEXT("The original keys could not be found."); + RefreshMenu(); + return; + } + const TArray CurrentActions = Settings->GetActionMappings(); + for (const FInputActionKeyMapping& Map : CurrentActions) + { + Settings->RemoveActionMapping(Map, false); + } + const TArray CurrentAxes = Settings->GetAxisMappings(); + for (const FInputAxisKeyMapping& Map : CurrentAxes) + { + Settings->RemoveAxisMapping(Map, false); + } + for (const FInputActionKeyMapping& Map : Actions) + { + Settings->AddActionMapping(Map, false); + } + for (const FInputAxisKeyMapping& Map : Axes) + { + Settings->AddAxisMapping(Map, false); + } + ApplyToPlayer(); + Status = TEXT("Original keys restored."); + RefreshMenu(); +} + +bool ULPKeyboardSubsystem::LoadSaved() const +{ + const FString Path = KeyboardIniPath(); + if (!FPaths::FileExists(Path)) + { + UE_LOG(LogLifePlay, Display, TEXT("Clavier : %s absent, creation avec les touches actuelles"), *Path); + return false; + } + FConfigFile File; + File.Read(Path); + TArray ActionLines; + TArray AxisLines; + File.GetArray(InputSection, TEXT("ActionMappings"), ActionLines); + File.GetArray(InputSection, TEXT("AxisMappings"), AxisLines); + TArray Actions; + TArray Axes; + for (const FString& Line : ActionLines) + { + FInputActionKeyMapping Map; + if (FInputActionKeyMapping::StaticStruct()->ImportText(*Line, &Map, nullptr, PPF_None, nullptr, TEXT("ActionMappings")) && Map.Key.IsValid()) + { + Actions.Add(Map); + } + } + for (const FString& Line : AxisLines) + { + FInputAxisKeyMapping Map; + if (FInputAxisKeyMapping::StaticStruct()->ImportText(*Line, &Map, nullptr, PPF_None, nullptr, TEXT("AxisMappings")) && Map.Key.IsValid()) + { + Axes.Add(Map); + } + } + UInputSettings* Settings = UInputSettings::GetInputSettings(); + if (!Settings || (Actions.Num() == 0 && Axes.Num() == 0)) + { + UE_LOG(LogLifePlay, Warning, TEXT("Clavier : %s illisible, les touches du jeu sont gardees"), *Path); + return false; + } + if (Actions.Num() > 0) + { + const TArray Current = Settings->GetActionMappings(); + for (const FInputActionKeyMapping& Map : Current) + { + Settings->RemoveActionMapping(Map, false); + } + for (const FInputActionKeyMapping& Map : Actions) + { + Settings->AddActionMapping(Map, false); + } + } + if (Axes.Num() > 0) + { + const TArray Current = Settings->GetAxisMappings(); + for (const FInputAxisKeyMapping& Map : Current) + { + Settings->RemoveAxisMapping(Map, false); + } + for (const FInputAxisKeyMapping& Map : Axes) + { + Settings->AddAxisMapping(Map, false); + } + } + Settings->ForceRebuildKeymaps(); + UE_LOG(LogLifePlay, Display, TEXT("Clavier : %d actions et %d axes relus depuis %s"), Actions.Num(), Axes.Num(), *Path); + return true; +} + +void ULPKeyboardSubsystem::SaveSaved() const +{ + UInputSettings* Settings = UInputSettings::GetInputSettings(); + if (!Settings) + { + return; + } + const FString Path = KeyboardIniPath(); + IFileManager::Get().MakeDirectory(*FPaths::GetPath(Path), true); + FConfigFile File; + int32 ActionCount = 0; + int32 AxisCount = 0; + for (const FInputActionKeyMapping& Map : Settings->GetActionMappings()) + { + if (Map.Key.IsValid()) + { + File.AddToSection(InputSection, TEXT("+ActionMappings"), ExportAction(Map)); + ++ActionCount; + } + } + for (const FInputAxisKeyMapping& Map : Settings->GetAxisMappings()) + { + if (Map.Key.IsValid()) + { + File.AddToSection(InputSection, TEXT("+AxisMappings"), ExportAxis(Map)); + ++AxisCount; + } + } + const bool bWrote = File.Write(Path); + UE_LOG(LogLifePlay, Display, TEXT("Clavier : %d actions et %d axes %s dans %s"), ActionCount, AxisCount, bWrote ? TEXT("enregistres") : TEXT("non ecrits"), *Path); +} + +void ULPKeyboardSubsystem::ApplyToPlayer() const +{ + if (UInputSettings* Settings = UInputSettings::GetInputSettings()) + { + Settings->ForceRebuildKeymaps(); + SaveSaved(); + } + if (UWorld* World = GetWorld()) + { + if (APlayerController* PC = World->GetFirstPlayerController()) + { + if (UPlayerInput* Input = PC->PlayerInput) + { + Input->ForceRebuildingKeyMaps(false); + } + } + } +} + +void ULPKeyboardSubsystem::RefreshMenu() +{ + if (Live().Owner.Get() == this && Live().Menu.IsValid()) + { + Live().Menu->Rebuild(); + } +} diff --git a/Source/LifePlay/LPKeyboardSubsystem.h b/Source/LifePlay/LPKeyboardSubsystem.h new file mode 100644 index 0000000..e0dc98d --- /dev/null +++ b/Source/LifePlay/LPKeyboardSubsystem.h @@ -0,0 +1,68 @@ +// Menu Clavier dans les reglages (MainMenu, liste SettingsScrollBox). +// +// Les touches du jeu sont les ActionMappings et AxisMappings de DefaultInput.ini, lus par BPs/PC. +// Ce sous-systeme ajoute un bouton « Clavier » dans cette liste et ouvre un panneau pour les +// changer. Le resultat est ecrit dans Saved/Config/Windows/LPKeyboard.ini de cette version, +// pas dans GameSettings.sav : les sauvegardes et le jeu 4.27 ne sont pas touches. +// Input.ini est le cache du moteur : il le supprime en quittant quand le diff est vide. +// Aucun Blueprint n'est modifie. Sans la liste des reglages, le bouton n'est pas ajoute. + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/Ticker.h" +#include "InputCoreTypes.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "Widgets/SWidget.h" +#include "LPKeyboardSubsystem.generated.h" + +class UScrollBox; + +UCLASS() +class LIFEPLAY_API ULPKeyboardSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + UFUNCTION() + void OpenKeyboardMenu(); + + TSharedRef TakeEmbeddedPage(); + void CloseKeyboardMenu(); + void BeginWait(FName Name, bool bAxis, float Scale, FKey OldKey, bool bShift, bool bCtrl, bool bAlt, bool bCmd, bool bAdd); + void CancelWait(); + bool CommitKey(const FKey& Key, bool bShift, bool bCtrl, bool bAlt, bool bCmd); + void RemoveBinding(FName Name, bool bAxis, float Scale, FKey Key, bool bShift, bool bCtrl, bool bAlt, bool bCmd); + void ResetToDefault(); + + bool IsWaiting() const { return bWaiting; } + bool IsMenuOpen() const; + const FString& GetStatus() const { return Status; } + void RefreshMenu(); + +private: + bool Tick(float DeltaTime); + void AttachButton(UScrollBox* Scroll); + void ApplyToPlayer() const; + bool LoadSaved() const; + void SaveSaved() const; + bool LoadDefaults(TArray& Actions, TArray& Axes) const; + + FTSTicker::FDelegateHandle TickHandle; + bool bLoggedMissing = false; + + bool bWaiting = false; + bool bWaitAdd = false; + bool bWaitAxis = false; + float WaitScale = 0.f; + FName WaitName; + FKey WaitOldKey; + bool bWaitShift = false; + bool bWaitCtrl = false; + bool bWaitAlt = false; + bool bWaitCmd = false; + FString Status; +};