2026-10-07 13:49:09 +00:00
|
|
|
|
// 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"
|
2026-10-20 15:35:10 +00:00
|
|
|
|
#include "Widgets/Input/SComboBox.h"
|
2026-10-07 13:49:09 +00:00
|
|
|
|
#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");
|
|
|
|
|
|
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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;
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// KEY2-SPEC-02 : section d'affichage (une fleche apparait dans Map & World et dans Couple, meme liaison).
|
|
|
|
|
|
int32 Section = 0;
|
2026-10-07 13:49:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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")},
|
2026-10-20 15:35:10 +00:00
|
|
|
|
{TEXT("ActionPanel"), TEXT("Action Panel (also Alt / middle click)")},
|
2026-10-07 13:49:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
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;
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// KEY2-SUITE-LB (CAM2-SPEC-02 § 7) : hors scene, la vue de dessus ; en scene, le couple.
|
2026-10-07 13:49:09 +00:00
|
|
|
|
if (Name == TEXT("LeftRight"))
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return bPositive ? TEXT("Right (map view / couple)") : TEXT("Left (map view / couple)");
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
if (Name == TEXT("DownUp"))
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return bPositive ? TEXT("Forward (map view / couple)") : TEXT("Back (map view / couple)");
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
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 −");
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
return Name.ToString() + (bPositive ? TEXT(" +") : TEXT(" −"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct FBindGroup
|
|
|
|
|
|
{
|
|
|
|
|
|
int32 Order = 9;
|
|
|
|
|
|
const TCHAR* Title = TEXT("Other");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
FBindGroup GroupFor(FName Name)
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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).
|
2026-10-07 13:49:09 +00:00
|
|
|
|
const FString Raw = Name.ToString();
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (Name == KbMoveForward || Name == KbMoveRight)
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {1, TEXT("Movement")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (Name == TEXT("SwitchCamera") || Name == TEXT("RotateCamera") || Name == TEXT("ScrollDownUp") || Name == KbCameraHeight)
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {2, TEXT("Camera")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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"))
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {KbCoupleOrder, TEXT("Couple (scene)")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (Name == TEXT("Interact") || Name == KbActionPanel || Name == TEXT("SwapRoles") || Name == TEXT("Recommended"))
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {5, TEXT("Actions & Interaction")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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")
|
2026-10-07 13:49:09 +00:00
|
|
|
|
|| Name == TEXT("SearchBarToggle"))
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {6, TEXT("Menus")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (Name == TEXT("Pause") || Name == TEXT("SpeedUp") || Name == TEXT("SpeedDown"))
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {7, TEXT("Time")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (Raw.StartsWith(TEXT("Favorite")))
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {8, TEXT("Favorites")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (Name == TEXT("QuickSave") || Name == TEXT("Quickload") || Name == TEXT("TestScenes"))
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return {9, TEXT("System")};
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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"))
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return Prefix + (bPositive ? TEXT("Right") : TEXT("Left"));
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return Prefix + (bPositive ? TEXT("Forward") : TEXT("Back"));
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-20 15:35:10 +00:00
|
|
|
|
FString ChordText(const FKey& Key, bool bShift, bool bCtrl, bool bAlt, bool bCmd)
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
|
|
|
|
|
FString Text;
|
|
|
|
|
|
if (bCtrl)
|
|
|
|
|
|
{
|
|
|
|
|
|
Text += TEXT("Ctrl+");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bShift)
|
|
|
|
|
|
{
|
|
|
|
|
|
Text += TEXT("Shift+");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bAlt)
|
|
|
|
|
|
{
|
|
|
|
|
|
Text += TEXT("Alt+");
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (bCmd)
|
|
|
|
|
|
{
|
|
|
|
|
|
Text += TEXT("Cmd+");
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
Text += Key.GetDisplayName().ToString();
|
|
|
|
|
|
return Text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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;
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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];
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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)))
|
2026-10-20 15:35:10 +00:00
|
|
|
|
]
|
|
|
|
|
|
// 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)))
|
|
|
|
|
|
]
|
2026-10-07 13:49:09 +00:00
|
|
|
|
]
|
|
|
|
|
|
+ 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)
|
2026-10-20 15:35:10 +00:00
|
|
|
|
.AutoWrapText(true)
|
2026-10-07 13:49:09 +00:00
|
|
|
|
.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);
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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);
|
2026-10-07 13:49:09 +00:00
|
|
|
|
Binds.Sort([](const FBindRow& A, const FBindRow& B)
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (A.Section != B.Section)
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
return A.Section < B.Section;
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (Row.Section != PreviousGroup)
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
PreviousGroup = Row.Section;
|
2026-10-07 13:49:09 +00:00
|
|
|
|
Previous.Reset();
|
2026-10-20 15:35:10 +00:00
|
|
|
|
const FBindGroup Section = Row.Section == KbMapOrder ? FBindGroup{KbMapOrder, TEXT("Map & World")} : GroupFor(Row.Name);
|
2026-10-07 13:49:09 +00:00
|
|
|
|
Rows->AddSlot().AutoHeight().Padding(0.f, 12.f, 0.f, 4.f)
|
|
|
|
|
|
[
|
|
|
|
|
|
MakeText(Section.Title, 20, FLinearColor(1.f, 0.92f, 0.45f))
|
|
|
|
|
|
];
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// KEY2-SPEC-02 : l'option « Move couple with » ouvre la section Couple (scene).
|
|
|
|
|
|
if (Row.Section == KbCoupleOrder)
|
|
|
|
|
|
{
|
|
|
|
|
|
AddCoupleDragRow(Sub);
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
]
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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)
|
|
|
|
|
|
[
|
2026-10-20 15:35:10 +00:00
|
|
|
|
MakeText(ChordText(Current.Key, Current.bShift, Current.bCtrl, Current.bAlt, Current.bCmd), 16, FLinearColor(0.85f, 0.9f, 1.f))
|
2026-10-07 13:49:09 +00:00
|
|
|
|
]
|
|
|
|
|
|
+ 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)
|
|
|
|
|
|
]
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
if (StatusBlock.IsValid() && Sub)
|
|
|
|
|
|
{
|
|
|
|
|
|
StatusBlock->SetText(FText::FromString(Sub->GetStatus()));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
FReply OnReset()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ULPKeyboardSubsystem* Sub = Owner.Get())
|
|
|
|
|
|
{
|
|
|
|
|
|
Sub->ResetToDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
return FReply::Handled();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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)
|
|
|
|
|
|
]
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 13:49:09 +00:00
|
|
|
|
FReply OnClose()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ULPKeyboardSubsystem* Sub = Owner.Get())
|
|
|
|
|
|
{
|
|
|
|
|
|
Sub->CloseKeyboardMenu();
|
|
|
|
|
|
}
|
|
|
|
|
|
return FReply::Handled();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TWeakObjectPtr<ULPKeyboardSubsystem> Owner;
|
|
|
|
|
|
TSharedPtr<SVerticalBox> Rows;
|
|
|
|
|
|
TSharedPtr<STextBlock> StatusBlock;
|
|
|
|
|
|
bool bEmbedded = false;
|
2026-10-20 15:35:10 +00:00
|
|
|
|
/** 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;
|
2026-10-07 13:49:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
PendingModifier = EKeys::Invalid;
|
2026-10-07 13:49:09 +00:00
|
|
|
|
Sub->CancelWait();
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (!Key.IsModifierKey())
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
PendingModifier = EKeys::Invalid;
|
2026-10-07 13:49:09 +00:00
|
|
|
|
Sub->CommitKey(Key, Event.IsShiftDown(), Event.IsControlDown(), Event.IsAltDown(), Event.IsCommandDown());
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Live().bEmbedded)
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Key == EKeys::Escape)
|
|
|
|
|
|
{
|
|
|
|
|
|
Sub->CloseKeyboardMenu();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-10-07 13:49:09 +00:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ULPKeyboardSubsystem::IsMenuOpen() const
|
|
|
|
|
|
{
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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);
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
Status = IdleStatus();
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
Status = IdleStatus();
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// KEY2 (§ 4) : toute modification manuelle du groupe de deplacement passe la disposition en Custom.
|
|
|
|
|
|
bool bMoveTouched = bWaitAxis && KbIsMoveGroup(WaitName);
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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));
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (KbIsMoveGroup(Map.AxisName))
|
|
|
|
|
|
{
|
|
|
|
|
|
bMoveTouched = true;
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bWaitAxis)
|
|
|
|
|
|
{
|
|
|
|
|
|
Settings->AddAxisMapping(FInputAxisKeyMapping(WaitName, Key, WaitScale), false);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Settings->AddActionMapping(FInputActionKeyMapping(WaitName, Key, bShift, bCtrl, bAlt, bCmd), false);
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (bMoveTouched)
|
|
|
|
|
|
{
|
|
|
|
|
|
Layout = KbLayoutCustom;
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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());
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (bAxis && KbIsMoveGroup(Name))
|
|
|
|
|
|
{
|
|
|
|
|
|
// KEY2 (§ 4) : modification manuelle du groupe de deplacement.
|
|
|
|
|
|
Layout = KbLayoutCustom;
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
ApplyToPlayer();
|
|
|
|
|
|
Status = FString::Printf(TEXT("%s removed."), *Key.GetDisplayName().ToString());
|
|
|
|
|
|
RefreshMenu();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-20 15:35:10 +00:00
|
|
|
|
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.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
ApplyToPlayer();
|
2026-10-20 15:35:10 +00:00
|
|
|
|
Status = bKeepAzerty ? TEXT("Original keys restored, AZERTY layout kept.") : TEXT("Original keys restored.");
|
2026-10-07 13:49:09 +00:00
|
|
|
|
RefreshMenu();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-20 15:35:10 +00:00
|
|
|
|
bool ULPKeyboardSubsystem::LoadSaved()
|
2026-10-07 13:49:09 +00:00
|
|
|
|
{
|
|
|
|
|
|
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;
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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);
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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);
|
2026-10-20 15:35:10 +00:00
|
|
|
|
if (bMigrate)
|
|
|
|
|
|
{
|
|
|
|
|
|
// KEY2 (§ 5) : le fichier migre est reecrit tout de suite (Version=2).
|
|
|
|
|
|
SaveSaved();
|
|
|
|
|
|
}
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-10-20 15:35:10 +00:00
|
|
|
|
// 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")));
|
2026-10-07 13:49:09 +00:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|