LifePlay-Revival/Source/LifePlay/LPKeyboardSubsystem.cpp
edujiro5972 56dfcfc3d2 LifePlay UE 5.8 : état de développement au 2026-10-20
Tickets : PUB-20261020-T1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:35:10 +02:00

1491 lines
49 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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/Input/SComboBox.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");
// KEY2 (KEY2-SPEC-01 § 4, § 5) : section [LPKeyboard] de LPKeyboard.ini et prereglages de disposition.
const TCHAR* KbSection = TEXT("LPKeyboard");
const TCHAR* KbLayoutQwerty = TEXT("QWERTY");
const TCHAR* KbLayoutAzerty = TEXT("AZERTY");
const TCHAR* KbLayoutCustom = TEXT("Custom");
const int32 KbVersion = 2;
const FName KbMoveForward(TEXT("MoveForward"));
const FName KbMoveRight(TEXT("MoveRight"));
const FName KbActionPanel(TEXT("ActionPanel"));
const FName KbCameraHeight(TEXT("CameraHeight"));
const FName KbCoupleMoveY(TEXT("CoupleMoveY"));
// KEY2-SPEC-02 : rangs des sections Map & World et Couple (scene) ; l'option « Move couple with » ouvre Couple.
const int32 KbMapOrder = 3;
const int32 KbCoupleOrder = 4;
bool KbIsMoveGroup(FName Name)
{
return Name == KbMoveForward || Name == KbMoveRight;
}
// Prereglage exclusif (§ 4) : QWERTY = W S A D et ActionPanel Q ; AZERTY = Z S Q D et ActionPanel A.
void KbLayoutPreset(bool bAzerty, TArray<FInputAxisKeyMapping>& Axes, FInputActionKeyMapping& Action)
{
Axes.Reset();
Axes.Add(FInputAxisKeyMapping(KbMoveForward, bAzerty ? EKeys::Z : EKeys::W, 1.f));
Axes.Add(FInputAxisKeyMapping(KbMoveForward, EKeys::S, -1.f));
Axes.Add(FInputAxisKeyMapping(KbMoveRight, bAzerty ? EKeys::Q : EKeys::A, -1.f));
Axes.Add(FInputAxisKeyMapping(KbMoveRight, EKeys::D, 1.f));
Action = FInputActionKeyMapping(KbActionPanel, bAzerty ? EKeys::A : EKeys::Q);
}
// Touches lues en dur par PC (KEY1-CART-01 § 12.2) : acceptees, avec un avertissement (§ 7, 5).
bool KbGameReadKey(const FKey& Key)
{
return Key == EKeys::LeftShift || Key == EKeys::RightShift || Key == EKeys::LeftControl || Key == EKeys::RightControl
|| Key == EKeys::F12 || Key == EKeys::PageDown || Key == EKeys::Y || Key == EKeys::N
|| Key == EKeys::LeftMouseButton || Key == EKeys::RightMouseButton;
}
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;
// KEY2-SPEC-02 : section d'affichage (une fleche apparait dans Map & World et dans Couple, meme liaison).
int32 Section = 0;
};
struct FLiveKeyboard
{
TWeakObjectPtr<ULPKeyboardSubsystem> Owner;
TSharedPtr<class SLPKeyboardMenu> Menu;
TSharedPtr<IInputProcessor> 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")},
{TEXT("ActionPanel"), TEXT("Action Panel (also Alt / middle click)")},
};
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;
// KEY2-SUITE-LB (CAM2-SPEC-02 § 7) : hors scene, la vue de dessus ; en scene, le couple.
if (Name == TEXT("LeftRight"))
{
return bPositive ? TEXT("Right (map view / couple)") : TEXT("Left (map view / couple)");
}
if (Name == TEXT("DownUp"))
{
return bPositive ? TEXT("Forward (map view / couple)") : TEXT("Back (map view / couple)");
}
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 −");
}
if (Name == KbCameraHeight)
{
return bPositive ? TEXT("Camera Height Up (scene)") : TEXT("Camera Height Down (scene)");
}
if (Name == KbCoupleMoveY)
{
// KEY2-SUITE-LB (option N1) : V / X montent et baissent le couple ; le nom interne CoupleMoveY est garde.
return bPositive ? TEXT("Couple Up") : TEXT("Couple Down");
}
return Name.ToString() + (bPositive ? TEXT(" +") : TEXT(" −"));
}
struct FBindGroup
{
int32 Order = 9;
const TCHAR* Title = TEXT("Other");
};
FBindGroup GroupFor(FName Name)
{
// KEY2-SPEC-02 (decision « regroupement des touches ok ») : 9 sections par fonctionnalite. Les fleches (LeftRight,
// DownUp) sont rangees dans Couple ; Rebuild en ajoute une copie dans Map & World (meme liaison).
const FString Raw = Name.ToString();
if (Name == KbMoveForward || Name == KbMoveRight)
{
return {1, TEXT("Movement")};
}
if (Name == TEXT("SwitchCamera") || Name == TEXT("RotateCamera") || Name == TEXT("ScrollDownUp") || Name == KbCameraHeight)
{
return {2, TEXT("Camera")};
}
if (Name == TEXT("NextBuilding") || Name == TEXT("PreviousBuilding"))
{
return {KbMapOrder, TEXT("Map & World")};
}
if (Name == KbCoupleMoveY || Name == TEXT("LeftRight") || Name == TEXT("DownUp") || Name == TEXT("ManualAnimating")
|| Name == TEXT("PenisRotation"))
{
return {KbCoupleOrder, TEXT("Couple (scene)")};
}
if (Name == TEXT("Interact") || Name == KbActionPanel || Name == TEXT("SwapRoles") || Name == TEXT("Recommended"))
{
return {5, TEXT("Actions & Interaction")};
}
if (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 {6, TEXT("Menus")};
}
if (Name == TEXT("Pause") || Name == TEXT("SpeedUp") || Name == TEXT("SpeedDown"))
{
return {7, TEXT("Time")};
}
if (Raw.StartsWith(TEXT("Favorite")))
{
return {8, TEXT("Favorites")};
}
if (Name == TEXT("QuickSave") || Name == TEXT("Quickload") || Name == TEXT("TestScenes"))
{
return {9, TEXT("System")};
}
return {10, TEXT("Other")};
}
// KEY2-SPEC-02 (option A) : libelle d'une fleche selon sa section, vue de dessus ou couple.
FString KbArrowLabel(FName Name, float Scale, bool bMap)
{
const bool bPositive = Scale >= 0.f;
const FString Prefix = bMap ? TEXT("Map ") : TEXT("Couple ");
if (Name == TEXT("LeftRight"))
{
return Prefix + (bPositive ? TEXT("Right") : TEXT("Left"));
}
return Prefix + (bPositive ? TEXT("Forward") : TEXT("Back"));
}
FString ChordText(const FKey& Key, bool bShift, bool bCtrl, bool bAlt, bool bCmd)
{
FString Text;
if (bCtrl)
{
Text += TEXT("Ctrl+");
}
if (bShift)
{
Text += TEXT("Shift+");
}
if (bAlt)
{
Text += TEXT("Alt+");
}
if (bCmd)
{
Text += TEXT("Cmd+");
}
Text += Key.GetDisplayName().ToString();
return Text;
}
// KEY2 : ligne sans touche (avec Add) si l'action, ou l'axe de ce signe, n'a aucune liaison affichee.
void KbAddMissing(TArray<FBindRow>& Binds, FName Name, bool bAxis, float Scale)
{
for (const FBindRow& Existing : Binds)
{
if (Existing.Name == Name && Existing.bAxis == bAxis && (!bAxis || (Existing.Scale >= 0.f) == (Scale >= 0.f)))
{
return;
}
}
FBindRow Missing;
Missing.Name = Name;
Missing.Label = bAxis ? AxisLabel(Name, Scale) : ActionLabel(Name);
Missing.bAxis = bAxis;
Missing.Scale = Scale;
Binds.Add(Missing);
}
TSharedRef<STextBlock> 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;
// KEY2 (§ 4) : liste des dispositions, la disposition enregistree choisie au depart.
LayoutOptions.Add(MakeShared<FString>(KbLayoutQwerty));
LayoutOptions.Add(MakeShared<FString>(KbLayoutAzerty));
SelectedLayout = (InOwner && InOwner->GetLayout() == KbLayoutAzerty) ? LayoutOptions[1] : LayoutOptions[0];
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)))
]
// KEY2 (§ 4) : disposition (QWERTY, AZERTY), bouton Apply et disposition en cours.
+ SVerticalBox::Slot().AutoHeight().Padding(0.f, 0.f, 0.f, 8.f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.f, 0.f, 8.f, 0.f)
[
SNew(SComboBox<TSharedPtr<FString>>)
.OptionsSource(&LayoutOptions)
.InitiallySelectedItem(SelectedLayout)
.OnGenerateWidget_Lambda([](TSharedPtr<FString> Item) -> TSharedRef<SWidget>
{
return MakeText(Item.IsValid() ? *Item : FString(), 16, FLinearColor::White);
})
.OnSelectionChanged_Lambda([this](TSharedPtr<FString> Item, ESelectInfo::Type)
{
if (Item.IsValid())
{
SelectedLayout = Item;
}
})
[
SNew(STextBlock)
.Text_Lambda([this]() { return FText::FromString(SelectedLayout.IsValid() ? *SelectedLayout : FString()); })
.Font(MakeText(TEXT(""), 16, FLinearColor::White)->GetFont())
]
]
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.f, 0.f, 8.f, 0.f)
[
SNew(SButton)
.OnClicked(this, &SLPKeyboardMenu::OnApplyLayout)
[
MakeText(TEXT("Apply"), 16, FLinearColor::White)
]
]
+ SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center)
[
SAssignNew(LayoutBlock, STextBlock)
.Font(MakeText(TEXT(""), 16, FLinearColor::White)->GetFont())
.ColorAndOpacity(FSlateColor(FLinearColor(0.85f, 0.9f, 1.f)))
]
]
+ 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)
.AutoWrapText(true)
.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<FBindRow> 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);
}
// KEY2 : entrees de la section Scene sans touche (fichier d'avant KEY2) : ligne vide avec Add.
KbAddMissing(Binds, KbActionPanel, false, 0.f);
KbAddMissing(Binds, KbCameraHeight, true, 1.f);
KbAddMissing(Binds, KbCameraHeight, true, -1.f);
KbAddMissing(Binds, KbCoupleMoveY, true, 1.f);
KbAddMissing(Binds, KbCoupleMoveY, true, -1.f);
// KEY2-SPEC-02 (option A) : section de chaque ligne ; une fleche apparait aussi dans Map & World (meme liaison :
// Change ou Remove dans l'une agit sur l'autre, le menu se redessine depuis les reglages).
TArray<FBindRow> MapRows;
for (FBindRow& Row : Binds)
{
Row.Section = GroupFor(Row.Name).Order;
if (Row.bAxis && (Row.Name == TEXT("LeftRight") || Row.Name == TEXT("DownUp")))
{
Row.Label = KbArrowLabel(Row.Name, Row.Scale, false);
FBindRow MapRow = Row;
MapRow.Section = KbMapOrder;
MapRow.Label = KbArrowLabel(Row.Name, Row.Scale, true);
MapRows.Add(MapRow);
}
}
Binds.Append(MapRows);
Binds.Sort([](const FBindRow& A, const FBindRow& B)
{
if (A.Section != B.Section)
{
return A.Section < B.Section;
}
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)
{
if (Row.Section != PreviousGroup)
{
PreviousGroup = Row.Section;
Previous.Reset();
const FBindGroup Section = Row.Section == KbMapOrder ? FBindGroup{KbMapOrder, TEXT("Map & World")} : GroupFor(Row.Name);
Rows->AddSlot().AutoHeight().Padding(0.f, 12.f, 0.f, 4.f)
[
MakeText(Section.Title, 20, FLinearColor(1.f, 0.92f, 0.45f))
];
// KEY2-SPEC-02 : l'option « Move couple with » ouvre la section Couple (scene).
if (Row.Section == KbCoupleOrder)
{
AddCoupleDragRow(Sub);
}
}
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)
]
]
];
}
if (!Row.Key.IsValid())
{
// KEY2 : entree sans touche (KbAddMissing) : seul Add est propose.
Rows->AddSlot().AutoHeight().Padding(12.f, 2.f, 0.f, 2.f)
[
MakeText(TEXT("(no key)"), 16, FLinearColor(0.85f, 0.9f, 1.f, 0.6f))
];
continue;
}
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, Current.bCmd), 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 (LayoutBlock.IsValid() && Sub)
{
// KEY2 (§ 4) : « Layout: QWERTY », « Layout: AZERTY » ou « Layout: custom ».
const FString ShownLayout = Sub->GetLayout() == KbLayoutAzerty ? FString(TEXT("AZERTY"))
: Sub->GetLayout() == KbLayoutQwerty ? FString(TEXT("QWERTY")) : FString(TEXT("custom"));
LayoutBlock->SetText(FText::FromString(TEXT("Layout: ") + ShownLayout));
}
if (StatusBlock.IsValid() && Sub)
{
StatusBlock->SetText(FText::FromString(Sub->GetStatus()));
}
}
private:
FReply OnReset()
{
if (ULPKeyboardSubsystem* Sub = Owner.Get())
{
Sub->ResetToDefault();
}
return FReply::Handled();
}
FReply OnApplyLayout()
{
if (ULPKeyboardSubsystem* Sub = Owner.Get())
{
Sub->ApplyLayout(SelectedLayout.IsValid() ? *SelectedLayout : FString(KbLayoutQwerty));
}
return FReply::Handled();
}
// KEY2 (§ 8b) : option « Move couple with » (Mouse and arrows, Arrows only), en tete du bloc des personnages.
void AddCoupleDragRow(const ULPKeyboardSubsystem* Keys)
{
Rows->AddSlot().AutoHeight().Padding(0.f, 10.f, 0.f, 2.f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center)
[
MakeText(TEXT("Move couple with"), 20, FLinearColor(1.f, 1.f, 1.f, 0.95f))
]
+ SHorizontalBox::Slot().AutoWidth()
[
SNew(SButton)
.OnClicked_Lambda([this]()
{
if (ULPKeyboardSubsystem* Owned = Owner.Get())
{
Owned->SetCoupleDragByKeys(!Owned->IsCoupleDragByKeys());
}
return FReply::Handled();
})
[
MakeText(Keys && Keys->IsCoupleDragByKeys() ? TEXT("Arrows only") : TEXT("Mouse and arrows"), 16, FLinearColor::White)
]
]
];
}
FReply OnClose()
{
if (ULPKeyboardSubsystem* Sub = Owner.Get())
{
Sub->CloseKeyboardMenu();
}
return FReply::Handled();
}
TWeakObjectPtr<ULPKeyboardSubsystem> Owner;
TSharedPtr<SVerticalBox> Rows;
TSharedPtr<STextBlock> StatusBlock;
bool bEmbedded = false;
/** KEY2 (§ 4) : options de la liste (QWERTY, AZERTY), choix en cours, texte de la disposition enregistree. */
TArray<TSharedPtr<FString>> LayoutOptions;
TSharedPtr<FString> SelectedLayout;
TSharedPtr<STextBlock> LayoutBlock;
};
FLiveKeyboard& Live()
{
static FLiveKeyboard State;
return State;
}
class FKeyboardProcessor : public IInputProcessor
{
public:
virtual void Tick(const float, FSlateApplication&, TSharedRef<ICursor>) 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)
{
PendingModifier = EKeys::Invalid;
Sub->CancelWait();
}
else if (!Key.IsModifierKey())
{
PendingModifier = EKeys::Invalid;
Sub->CommitKey(Key, Event.IsShiftDown(), Event.IsControlDown(), Event.IsAltDown(), Event.IsCommandDown());
}
else if (!Event.IsRepeat())
{
// KEY2 (§ 7, 3) : modificateur seul, enregistre au relachement si rien d'autre n'est presse entre-temps.
bModifierAlone = !PendingModifier.IsValid();
PendingModifier = Key;
}
return true;
}
if (Live().bEmbedded)
{
return false;
}
if (Key == EKeys::Escape)
{
Sub->CloseKeyboardMenu();
return true;
}
return true;
}
virtual bool HandleKeyUpEvent(FSlateApplication&, const FKeyEvent& Event) override
{
const FKey Key = Event.GetKey();
const bool bReleased = PendingModifier.IsValid() && Key == PendingModifier;
if (bReleased)
{
PendingModifier = EKeys::Invalid;
}
ULPKeyboardSubsystem* Sub = Live().Owner.Get();
if (!bReleased || !bModifierAlone || !Sub || !Live().Menu.IsValid() || !Sub->IsWaiting())
{
return false;
}
// KEY2 (§ 7, 3) : modificateur seul (ex. LeftControl de ManualAnimating), sans modificateur en plus.
Sub->CommitKey(Key, false, false, false, false);
return true;
}
virtual bool HandleMouseButtonDownEvent(FSlateApplication&, const FPointerEvent& Event) override
{
ULPKeyboardSubsystem* Sub = Live().Owner.Get();
if (!Sub || !Live().Menu.IsValid() || !Sub->IsWaiting())
{
return false;
}
// KEY2 (§ 7, 1) : boutons de souris pendant une capture ; clic gauche seulement avec un modificateur.
bModifierAlone = false;
const FKey Button = Event.GetEffectingButton();
const bool bModifier = Event.IsShiftDown() || Event.IsControlDown() || Event.IsAltDown() || Event.IsCommandDown();
if (Button == EKeys::LeftMouseButton && !bModifier)
{
Sub->RefuseLeftClick();
}
else if (Button == EKeys::LeftMouseButton || Button == EKeys::RightMouseButton || Button == EKeys::MiddleMouseButton
|| Button == EKeys::ThumbMouseButton || Button == EKeys::ThumbMouseButton2)
{
PendingModifier = EKeys::Invalid;
Sub->CommitKey(Button, Event.IsShiftDown(), Event.IsControlDown(), Event.IsAltDown(), Event.IsCommandDown());
}
return true;
}
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication&, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) override
{
ULPKeyboardSubsystem* Sub = Live().Owner.Get();
if (!Sub || !Live().Menu.IsValid() || !Sub->IsWaiting() || InGestureEvent || FMath::IsNearlyZero(InWheelEvent.GetWheelDelta()))
{
return false;
}
// KEY2 (§ 7, 2) : molette pour les actions seulement (les axes gardent MouseWheelAxis).
bModifierAlone = false;
PendingModifier = EKeys::Invalid;
Sub->CommitWheel(InWheelEvent.GetWheelDelta() > 0.f, InWheelEvent.IsShiftDown(), InWheelEvent.IsControlDown(),
InWheelEvent.IsAltDown(), InWheelEvent.IsCommandDown());
return true;
}
private:
/** KEY2 : modificateur tenu pendant la capture ; vrai tant qu'aucune autre touche ni bouton n'a ete presse. */
FKey PendingModifier;
bool bModifierAlone = false;
};
}
bool ULPKeyboardSubsystem::IsMenuOpen() const
{
// KEY2-DIAG-01 (COMMUN-15) : la page integree aux reglages reste valide apres leur fermeture ; elle ne compte comme
// menu que pendant une capture. Les reglages ouverts sont deja vus comme menu (MainMenu_C, MenuOnScreen).
return Live().Menu.IsValid() && Live().Owner.Get() == this && (!Live().bEmbedded || bWaiting);
}
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<UScrollBox> It; It; ++It)
{
UScrollBox* Scroll = *It;
if (!Scroll || Scroll->GetFName() != TEXT("SettingsScrollBox") || Scroll->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))
{
continue;
}
const UUserWidget* Owner = Scroll->GetTypedOuter<UUserWidget>();
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<UButton>(Scroll, ButtonName);
UTextBlock* Label = NewObject<UTextBlock>(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<UScrollBoxSlot>(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<SWidget> ULPKeyboardSubsystem::TakeEmbeddedPage()
{
if (Live().Menu.IsValid() && Live().Owner.Get() == this)
{
return Live().Menu.ToSharedRef();
}
Status = IdleStatus();
bWaiting = false;
Live().bEmbedded = true;
TSharedRef<SLPKeyboardMenu> Panel = SNew(SLPKeyboardMenu, this).bEmbedded(true);
Live().Owner = this;
Live().Menu = Panel;
if (!Live().Processor.IsValid())
{
Live().Processor = MakeShared<FKeyboardProcessor>();
FSlateApplication::Get().RegisterInputPreProcessor(Live().Processor.ToSharedRef(), 0);
}
return Panel;
}
void ULPKeyboardSubsystem::OpenKeyboardMenu()
{
if (Live().Menu.IsValid() || !FSlateApplication::IsInitialized() || !GEngine || !GEngine->GameViewport)
{
return;
}
Status = IdleStatus();
bWaiting = false;
Live().bEmbedded = false;
TSharedRef<SLPKeyboardMenu> Panel = SNew(SLPKeyboardMenu, this).bEmbedded(false);
Live().Owner = this;
Live().Menu = Panel;
Live().Processor = MakeShared<FKeyboardProcessor>();
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<FString> 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<FInputActionKeyMapping> 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));
}
}
// KEY2 (§ 4) : toute modification manuelle du groupe de deplacement passe la disposition en Custom.
bool bMoveTouched = bWaitAxis && KbIsMoveGroup(WaitName);
TArray<FInputAxisKeyMapping> 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 (KbIsMoveGroup(Map.AxisName))
{
bMoveTouched = true;
}
}
}
if (bWaitAxis)
{
Settings->AddAxisMapping(FInputAxisKeyMapping(WaitName, Key, WaitScale), false);
}
else
{
Settings->AddActionMapping(FInputActionKeyMapping(WaitName, Key, bShift, bCtrl, bAlt, bCmd), false);
}
if (bMoveTouched)
{
Layout = KbLayoutCustom;
}
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());
if (KbGameReadKey(Key))
{
// KEY2 (§ 7, 5) : touche lue en dur par le jeu, acceptee avec un avertissement.
Status += FString::Printf(TEXT(" Also used by the game: %s."), *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);
}
if (bAxis && KbIsMoveGroup(Name))
{
// KEY2 (§ 4) : modification manuelle du groupe de deplacement.
Layout = KbLayoutCustom;
}
ApplyToPlayer();
Status = FString::Printf(TEXT("%s removed."), *Key.GetDisplayName().ToString());
RefreshMenu();
}
void ULPKeyboardSubsystem::ApplyPreset(bool bAzerty, TArray<FString>& Cleared)
{
UInputSettings* Settings = UInputSettings::GetInputSettings();
if (!Settings)
{
return;
}
TArray<FInputAxisKeyMapping> PresetAxes;
FInputActionKeyMapping PresetAction;
KbLayoutPreset(bAzerty, PresetAxes, PresetAction);
// 1. Toutes les liaisons de MoveForward, MoveRight et ActionPanel.
const TArray<FInputAxisKeyMapping> GroupAxes = Settings->GetAxisMappings();
for (const FInputAxisKeyMapping& Map : GroupAxes)
{
if (KbIsMoveGroup(Map.AxisName))
{
Settings->RemoveAxisMapping(Map, false);
}
}
const TArray<FInputActionKeyMapping> GroupActions = Settings->GetActionMappings();
for (const FInputActionKeyMapping& Map : GroupActions)
{
if (Map.ActionName == KbActionPanel)
{
Settings->RemoveActionMapping(Map, false);
}
}
// 2. La meme touche, sans modificateur, retiree des autres actions et axes (regle de CommitKey), avec annonce.
TArray<FKey> PresetKeys;
for (const FInputAxisKeyMapping& Map : PresetAxes)
{
PresetKeys.AddUnique(Map.Key);
}
PresetKeys.AddUnique(PresetAction.Key);
const TArray<FInputActionKeyMapping> OtherActions = Settings->GetActionMappings();
for (const FInputActionKeyMapping& Map : OtherActions)
{
if (PresetKeys.Contains(Map.Key) && !Map.bShift && !Map.bCtrl && !Map.bAlt && !Map.bCmd)
{
Settings->RemoveActionMapping(Map, false);
Cleared.Add(FString::Printf(TEXT("%s (%s)"), *ActionLabel(Map.ActionName), *Map.Key.GetDisplayName().ToString()));
}
}
const TArray<FInputAxisKeyMapping> OtherAxes = Settings->GetAxisMappings();
for (const FInputAxisKeyMapping& Map : OtherAxes)
{
if (PresetKeys.Contains(Map.Key))
{
Settings->RemoveAxisMapping(Map, false);
Cleared.Add(FString::Printf(TEXT("%s (%s)"), *AxisLabel(Map.AxisName, Map.Scale), *Map.Key.GetDisplayName().ToString()));
}
}
// 3. Les 5 liaisons du prereglage (ApplyToPlayer par l'appelant).
for (const FInputAxisKeyMapping& Map : PresetAxes)
{
Settings->AddAxisMapping(Map, false);
}
Settings->AddActionMapping(PresetAction, false);
}
void ULPKeyboardSubsystem::ApplyLayout(const FString& LayoutName)
{
bWaiting = false;
if (!UInputSettings::GetInputSettings())
{
return;
}
const bool bAzerty = LayoutName == KbLayoutAzerty;
TArray<FString> Cleared;
ApplyPreset(bAzerty, Cleared);
Layout = bAzerty ? KbLayoutAzerty : KbLayoutQwerty;
ApplyToPlayer();
Status = Cleared.Num() > 0
? FString::Printf(TEXT("Layout %s applied. Removed from: %s."), *Layout, *FString::Join(Cleared, TEXT(", ")))
: FString::Printf(TEXT("Layout %s applied."), *Layout);
UE_LOG(LogLifePlay, Display, TEXT("Clavier : disposition %s appliquee, %d liaisons retirees ailleurs"), *Layout, Cleared.Num());
RefreshMenu();
}
void ULPKeyboardSubsystem::RefuseLeftClick()
{
Status = TEXT("Left click needs Shift, Ctrl, Alt or Cmd. Escape cancels.");
RefreshMenu();
}
bool ULPKeyboardSubsystem::CommitWheel(bool bUp, bool bShift, bool bCtrl, bool bAlt, bool bCmd)
{
if (bWaitAxis)
{
Status = TEXT("The mouse wheel can only be assigned to an action. Press a key, or Escape.");
RefreshMenu();
return false;
}
return CommitKey(bUp ? EKeys::MouseScrollUp : EKeys::MouseScrollDown, bShift, bCtrl, bAlt, bCmd);
}
void ULPKeyboardSubsystem::SetCoupleDragByKeys(bool bKeys)
{
bWaiting = false;
bCoupleDragKeys = bKeys;
SaveSaved();
Status = bKeys
? TEXT("Couple moved with the arrows only: the left click no longer drags it.")
: TEXT("Couple moved with the mouse and the arrows.");
RefreshMenu();
}
FString ULPKeyboardSubsystem::IdleStatus() const
{
if (const UInputSettings* Settings = UInputSettings::GetInputSettings())
{
for (const FInputActionKeyMapping& Map : Settings->GetActionMappings())
{
if (Map.ActionName == KbActionPanel && Map.Key.IsValid())
{
return TEXT("Choose Change or Add, then press a key.");
}
}
}
return TEXT("Action Panel has no key: choose a layout and press Apply, or use Add.");
}
bool ULPKeyboardSubsystem::LoadDefaults(TArray<FInputActionKeyMapping>& Actions, TArray<FInputAxisKeyMapping>& Axes) const
{
const FString Path = FPaths::Combine(FPaths::ProjectConfigDir(), TEXT("DefaultInput.ini"));
TArray<FString> 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<FInputActionKeyMapping> Actions;
TArray<FInputAxisKeyMapping> Axes;
UInputSettings* Settings = UInputSettings::GetInputSettings();
if (!Settings || !LoadDefaults(Actions, Axes))
{
Status = TEXT("The original keys could not be found.");
RefreshMenu();
return;
}
const TArray<FInputActionKeyMapping> CurrentActions = Settings->GetActionMappings();
for (const FInputActionKeyMapping& Map : CurrentActions)
{
Settings->RemoveActionMapping(Map, false);
}
const TArray<FInputAxisKeyMapping> 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);
}
// KEY2 (§ 4, decision 12) : un joueur en AZERTY le reste ; les autres reviennent au QWERTY de DefaultInput.ini.
const bool bKeepAzerty = Layout == KbLayoutAzerty;
if (bKeepAzerty)
{
TArray<FString> Cleared;
ApplyPreset(true, Cleared);
}
else
{
Layout = KbLayoutQwerty;
}
ApplyToPlayer();
Status = bKeepAzerty ? TEXT("Original keys restored, AZERTY layout kept.") : TEXT("Original keys restored.");
RefreshMenu();
}
bool ULPKeyboardSubsystem::LoadSaved()
{
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<FString> ActionLines;
TArray<FString> AxisLines;
// KEY2-DIAG-01 : FConfigFile::Read ne traite pas les commandes '+' (FillFileFromDisk, bHandleSymbolCommands faux) :
// les cles ecrites par SaveSaved se relisent sous leur nom avec '+'. Le nom sans '+' reste lu pour un fichier edite a la main.
File.GetArray(InputSection, TEXT("+ActionMappings"), ActionLines);
File.GetArray(InputSection, TEXT("+AxisMappings"), AxisLines);
TArray<FString> PlainLines;
File.GetArray(InputSection, TEXT("ActionMappings"), PlainLines);
ActionLines.Append(PlainLines);
PlainLines.Reset();
File.GetArray(InputSection, TEXT("AxisMappings"), PlainLines);
AxisLines.Append(PlainLines);
TArray<FInputActionKeyMapping> Actions;
TArray<FInputAxisKeyMapping> 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;
}
// KEY2 (KEY2-SPEC-01 § 4, § 5, § 5b) : section [LPKeyboard]. Un fichier d'avant la version 2 perd les liaisons
// retirees du jeu (exactes, sans modificateur), recoit ActionPanel sur Q, CameraHeight sur G / B et CoupleMoveY sur
// V / X quand l'action manque et que la touche n'est liee a rien (arbitrage de Big Boss), passe en Custom.
FString SavedLayout;
if (File.GetString(KbSection, TEXT("Layout"), SavedLayout))
{
Layout = SavedLayout == KbLayoutAzerty ? KbLayoutAzerty : SavedLayout == KbLayoutQwerty ? KbLayoutQwerty : KbLayoutCustom;
}
FString SavedDrag;
bCoupleDragKeys = File.GetString(KbSection, TEXT("CoupleDrag"), SavedDrag) && SavedDrag == TEXT("Keys");
int32 SavedVersion = 0;
File.GetInt(KbSection, TEXT("Version"), SavedVersion);
const bool bMigrate = SavedVersion < KbVersion;
if (bMigrate)
{
struct FRetiredKey
{
const TCHAR* Action;
FKey Key;
};
const FRetiredKey Retired[] = {
{TEXT("SpeedUp"), EKeys::NumPadSix}, {TEXT("SpeedDown"), EKeys::NumPadFour},
{TEXT("SpeedUp"), EKeys::Equals}, {TEXT("SpeedDown"), EKeys::Hyphen},
{TEXT("TestScenes"), EKeys::Tilde}
};
int32 Removed = 0;
for (const FRetiredKey& Entry : Retired)
{
const FName EntryAction(Entry.Action);
Removed += Actions.RemoveAll([&EntryAction, &Entry](const FInputActionKeyMapping& Map)
{
return Map.ActionName == EntryAction && Map.Key == Entry.Key && !Map.bShift && !Map.bCtrl && !Map.bAlt && !Map.bCmd;
});
}
// Une touche est libre si aucune action ni aucun axe du fichier du joueur ne l'utilise.
auto KeyFree = [&Actions, &Axes](const FKey& Key)
{
return !Actions.ContainsByPredicate([&Key](const FInputActionKeyMapping& Map) { return Map.Key == Key; })
&& !Axes.ContainsByPredicate([&Key](const FInputAxisKeyMapping& Map) { return Map.Key == Key; });
};
const bool bHasPanel = Actions.ContainsByPredicate([](const FInputActionKeyMapping& Map) { return Map.ActionName == KbActionPanel; });
const bool bPanelAdded = Actions.Num() > 0 && !bHasPanel && KeyFree(EKeys::Q);
if (bPanelAdded)
{
Actions.Add(FInputActionKeyMapping(KbActionPanel, EKeys::Q));
}
// Axes nouveaux de KEY2 (§ 8b) : chaque sens n'est ajoute que sur sa touche libre ; sinon « (no key) » dans le menu.
struct FNewAxisKey
{
FName Axis;
FKey Key;
float Scale;
};
const FNewAxisKey NewAxes[] = {
{FName(TEXT("CameraHeight")), EKeys::G, 1.f}, {FName(TEXT("CameraHeight")), EKeys::B, -1.f},
{FName(TEXT("CoupleMoveY")), EKeys::V, 1.f}, {FName(TEXT("CoupleMoveY")), EKeys::X, -1.f}
};
int32 AxesAdded = 0;
for (const FNewAxisKey& Entry : NewAxes)
{
// Un sens deja lie par le joueur garde ses touches.
const bool bHasSign = Axes.ContainsByPredicate([&Entry](const FInputAxisKeyMapping& Map)
{
return Map.AxisName == Entry.Axis && (Map.Scale > 0.f) == (Entry.Scale > 0.f);
});
if (Axes.Num() > 0 && !bHasSign && KeyFree(Entry.Key))
{
Axes.Add(FInputAxisKeyMapping(Entry.Axis, Entry.Key, Entry.Scale));
++AxesAdded;
}
}
Layout = KbLayoutCustom;
UE_LOG(LogLifePlay, Display, TEXT("Clavier : %s passe de la version %d a %d : %d liaisons retirees (pave, Equals, Hyphen, Tilde), ActionPanel %s, %d touches d'axe ajoutees (CameraHeight G/B, CoupleMoveY V/X), Layout=Custom"),
*Path, SavedVersion, KbVersion, Removed, bPanelAdded ? TEXT("ajoute sur Q") : bHasPanel ? TEXT("deja lie") : TEXT("sans touche"), AxesAdded);
}
if (Actions.Num() > 0)
{
const TArray<FInputActionKeyMapping> 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<FInputAxisKeyMapping> 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);
if (bMigrate)
{
// KEY2 (§ 5) : le fichier migre est reecrit tout de suite (Version=2).
SaveSaved();
}
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;
}
}
// KEY2 (§ 4, § 5, § 8b) : disposition, version du fichier et option du glisser du couple.
File.AddToSection(KbSection, TEXT("Layout"), Layout);
File.AddToSection(KbSection, TEXT("Version"), FString::FromInt(KbVersion));
File.AddToSection(KbSection, TEXT("CoupleDrag"), FString(bCoupleDragKeys ? TEXT("Keys") : TEXT("Mouse")));
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();
}
}