// 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& 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 Owner; TSharedPtr Menu; TSharedPtr Processor; bool bEmbedded = false; }; bool IsRemappable(const FKey& Key) { return Key.IsValid() && !Key.IsAnalog() && !Key.IsAxis1D() && !Key.IsGamepadKey(); } FString ActionLabel(FName Name) { static const TCHAR* Known[][2] = { {TEXT("Pause"), TEXT("Pause")}, {TEXT("Journal"), TEXT("Journal")}, {TEXT("Inventory"), TEXT("Inventory")}, {TEXT("MainMenu"), TEXT("Main Menu")}, {TEXT("CharacterMenu"), TEXT("Character Menu")}, {TEXT("ModuleMenu"), TEXT("Modules")}, {TEXT("CityMenu"), TEXT("City")}, {TEXT("StatMenu"), TEXT("Statistics")}, {TEXT("ContactsMenu"), TEXT("Contacts")}, {TEXT("RotateCamera"), TEXT("Rotate Camera")}, {TEXT("ManualAnimating"), TEXT("Manual Animation")}, {TEXT("SpeedUp"), TEXT("Speed Up")}, {TEXT("SpeedDown"), TEXT("Slow Down")}, {TEXT("SwapRoles"), TEXT("Swap Roles")}, {TEXT("SwitchCamera"), TEXT("Switch Camera")}, {TEXT("NextBuilding"), TEXT("Next Building")}, {TEXT("PreviousBuilding"), TEXT("Previous Building")}, {TEXT("TestScenes"), TEXT("Test Scenes")}, {TEXT("Recommended"), TEXT("Recommended")}, {TEXT("QuickSave"), TEXT("Quick Save")}, {TEXT("Quickload"), TEXT("Quick Load")}, {TEXT("SearchBarToggle"), TEXT("Search")}, {TEXT("Interact"), TEXT("Interact")}, {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& 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 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(KbLayoutQwerty)); LayoutOptions.Add(MakeShared(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>) .OptionsSource(&LayoutOptions) .InitiallySelectedItem(SelectedLayout) .OnGenerateWidget_Lambda([](TSharedPtr Item) -> TSharedRef { return MakeText(Item.IsValid() ? *Item : FString(), 16, FLinearColor::White); }) .OnSelectionChanged_Lambda([this](TSharedPtr 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 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 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 Owner; TSharedPtr Rows; TSharedPtr StatusBlock; bool bEmbedded = false; /** KEY2 (§ 4) : options de la liste (QWERTY, AZERTY), choix en cours, texte de la disposition enregistree. */ TArray> LayoutOptions; TSharedPtr SelectedLayout; TSharedPtr LayoutBlock; }; FLiveKeyboard& Live() { static FLiveKeyboard State; return State; } class FKeyboardProcessor : public IInputProcessor { public: virtual void Tick(const float, FSlateApplication&, TSharedRef) override {} virtual bool HandleKeyDownEvent(FSlateApplication&, const FKeyEvent& Event) override { ULPKeyboardSubsystem* Sub = Live().Owner.Get(); if (!Sub || !Live().Menu.IsValid()) { return false; } const FKey Key = Event.GetKey(); if (Sub->IsWaiting()) { if (Key == EKeys::Escape) { 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 It; It; ++It) { UScrollBox* Scroll = *It; if (!Scroll || Scroll->GetFName() != TEXT("SettingsScrollBox") || Scroll->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject)) { continue; } const UUserWidget* Owner = Scroll->GetTypedOuter(); if (!Owner || Owner->HasAnyFlags(RF_ClassDefaultObject) || Owner->GetGameInstance() != GI) { continue; } bFound = true; } if (!bFound && !bLoggedMissing) { bLoggedMissing = true; UE_LOG(LogLifePlay, Display, TEXT("Clavier : liste des reglages pas encore ouverte")); } return true; } void ULPKeyboardSubsystem::AttachButton(UScrollBox* Scroll) { for (int32 Index = 0; Index < Scroll->GetChildrenCount(); ++Index) { if (Scroll->GetChildAt(Index) && Scroll->GetChildAt(Index)->GetFName() == ButtonName) { return; } } UButton* Button = NewObject(Scroll, ButtonName); UTextBlock* Label = NewObject(Button, TEXT("LPKeyboardLabel")); Label->SetText(FText::FromString(TEXT("Keyboard"))); Label->SetColorAndOpacity(FSlateColor(FLinearColor::White)); FSlateFontInfo Font = FCoreStyle::GetDefaultFontStyle("Regular", 16); Label->SetFont(Font); Button->SetContent(Label); if (UScrollBoxSlot* Slot = Cast(Scroll->AddChild(Button))) { Slot->SetPadding(FMargin(0.f, 12.f, 0.f, 4.f)); Slot->SetHorizontalAlignment(HAlign_Fill); } Button->OnClicked.AddDynamic(this, &ULPKeyboardSubsystem::OpenKeyboardMenu); UE_LOG(LogLifePlay, Display, TEXT("Clavier : bouton ajoute dans les reglages")); } TSharedRef ULPKeyboardSubsystem::TakeEmbeddedPage() { if (Live().Menu.IsValid() && Live().Owner.Get() == this) { return Live().Menu.ToSharedRef(); } Status = IdleStatus(); bWaiting = false; Live().bEmbedded = true; TSharedRef Panel = SNew(SLPKeyboardMenu, this).bEmbedded(true); Live().Owner = this; Live().Menu = Panel; if (!Live().Processor.IsValid()) { Live().Processor = MakeShared(); FSlateApplication::Get().RegisterInputPreProcessor(Live().Processor.ToSharedRef(), 0); } return Panel; } void ULPKeyboardSubsystem::OpenKeyboardMenu() { if (Live().Menu.IsValid() || !FSlateApplication::IsInitialized() || !GEngine || !GEngine->GameViewport) { return; } Status = IdleStatus(); bWaiting = false; Live().bEmbedded = false; TSharedRef Panel = SNew(SLPKeyboardMenu, this).bEmbedded(false); Live().Owner = this; Live().Menu = Panel; Live().Processor = MakeShared(); GEngine->GameViewport->AddViewportWidgetContent(Panel, 200); FSlateApplication::Get().RegisterInputPreProcessor(Live().Processor.ToSharedRef(), 0); } void ULPKeyboardSubsystem::CloseKeyboardMenu() { if (Live().Owner.Get() != this && Live().Owner.IsValid()) { return; } bWaiting = false; if (Live().Processor.IsValid() && FSlateApplication::IsInitialized()) { FSlateApplication::Get().UnregisterInputPreProcessor(Live().Processor.ToSharedRef()); } if (Live().Menu.IsValid() && GEngine && GEngine->GameViewport) { GEngine->GameViewport->RemoveViewportWidgetContent(Live().Menu.ToSharedRef()); } Live().Menu.Reset(); Live().Processor.Reset(); Live().Owner.Reset(); Live().bEmbedded = false; } void ULPKeyboardSubsystem::BeginWait(FName Name, bool bAxis, float Scale, FKey OldKey, bool bShift, bool bCtrl, bool bAlt, bool bCmd, bool bAdd) { bWaiting = true; bWaitAdd = bAdd; bWaitAxis = bAxis; WaitScale = Scale; WaitName = Name; WaitOldKey = OldKey; bWaitShift = bShift; bWaitCtrl = bCtrl; bWaitAlt = bAlt; bWaitCmd = bCmd; Status = bAdd ? FString::Printf(TEXT("New key for %s. Escape cancels."), bAxis ? *AxisLabel(Name, Scale) : *ActionLabel(Name)) : FString::Printf(TEXT("Replaces %s. Escape cancels."), *OldKey.GetDisplayName().ToString()); } void ULPKeyboardSubsystem::CancelWait() { bWaiting = false; Status = TEXT("Capture cancelled."); RefreshMenu(); } bool ULPKeyboardSubsystem::CommitKey(const FKey& Key, bool bShift, bool bCtrl, bool bAlt, bool bCmd) { bWaiting = false; if (!IsRemappable(Key)) { Status = TEXT("This key cannot be assigned."); RefreshMenu(); return false; } UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!Settings) { return false; } TArray Cleared; if (!bWaitAdd && WaitOldKey.IsValid()) { if (bWaitAxis) { Settings->RemoveAxisMapping(FInputAxisKeyMapping(WaitName, WaitOldKey, WaitScale), false); } else { Settings->RemoveActionMapping(FInputActionKeyMapping(WaitName, WaitOldKey, bWaitShift, bWaitCtrl, bWaitAlt, bWaitCmd), false); } } const TArray Actions = Settings->GetActionMappings(); for (const FInputActionKeyMapping& Map : Actions) { if (Map.Key == Key && Map.bShift == bShift && Map.bCtrl == bCtrl && Map.bAlt == bAlt && Map.bCmd == bCmd) { Settings->RemoveActionMapping(Map, false); Cleared.Add(ActionLabel(Map.ActionName)); } } // KEY2 (§ 4) : toute modification manuelle du groupe de deplacement passe la disposition en Custom. bool bMoveTouched = bWaitAxis && KbIsMoveGroup(WaitName); TArray Axes = Settings->GetAxisMappings(); for (const FInputAxisKeyMapping& Map : Axes) { if (Map.Key == Key && !bShift && !bCtrl && !bAlt && !bCmd) { Settings->RemoveAxisMapping(Map, false); Cleared.Add(AxisLabel(Map.AxisName, Map.Scale)); if (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& Cleared) { UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!Settings) { return; } TArray PresetAxes; FInputActionKeyMapping PresetAction; KbLayoutPreset(bAzerty, PresetAxes, PresetAction); // 1. Toutes les liaisons de MoveForward, MoveRight et ActionPanel. const TArray GroupAxes = Settings->GetAxisMappings(); for (const FInputAxisKeyMapping& Map : GroupAxes) { if (KbIsMoveGroup(Map.AxisName)) { Settings->RemoveAxisMapping(Map, false); } } const TArray 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 PresetKeys; for (const FInputAxisKeyMapping& Map : PresetAxes) { PresetKeys.AddUnique(Map.Key); } PresetKeys.AddUnique(PresetAction.Key); const TArray 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 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 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& Actions, TArray& Axes) const { const FString Path = FPaths::Combine(FPaths::ProjectConfigDir(), TEXT("DefaultInput.ini")); TArray Lines; if (!FFileHelper::LoadFileToStringArray(Lines, *Path)) { return false; } for (FString Line : Lines) { Line.TrimStartAndEndInline(); if (Line.StartsWith(TEXT("+ActionMappings="))) { FInputActionKeyMapping Map; const FString Inner = Line.Mid(16); if (FInputActionKeyMapping::StaticStruct()->ImportText(*Inner, &Map, nullptr, PPF_None, nullptr, TEXT("ActionMappings")) && Map.Key.IsValid()) { Actions.Add(Map); } } else if (Line.StartsWith(TEXT("+AxisMappings="))) { FInputAxisKeyMapping Map; const FString Inner = Line.Mid(14); if (FInputAxisKeyMapping::StaticStruct()->ImportText(*Inner, &Map, nullptr, PPF_None, nullptr, TEXT("AxisMappings")) && Map.Key.IsValid()) { Axes.Add(Map); } } } return Actions.Num() > 0 || Axes.Num() > 0; } void ULPKeyboardSubsystem::ResetToDefault() { bWaiting = false; TArray Actions; TArray Axes; UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!Settings || !LoadDefaults(Actions, Axes)) { Status = TEXT("The original keys could not be found."); RefreshMenu(); return; } const TArray CurrentActions = Settings->GetActionMappings(); for (const FInputActionKeyMapping& Map : CurrentActions) { Settings->RemoveActionMapping(Map, false); } const TArray CurrentAxes = Settings->GetAxisMappings(); for (const FInputAxisKeyMapping& Map : CurrentAxes) { Settings->RemoveAxisMapping(Map, false); } for (const FInputActionKeyMapping& Map : Actions) { Settings->AddActionMapping(Map, false); } for (const FInputAxisKeyMapping& Map : Axes) { Settings->AddAxisMapping(Map, false); } // 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 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 ActionLines; TArray 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 PlainLines; File.GetArray(InputSection, TEXT("ActionMappings"), PlainLines); ActionLines.Append(PlainLines); PlainLines.Reset(); File.GetArray(InputSection, TEXT("AxisMappings"), PlainLines); AxisLines.Append(PlainLines); TArray Actions; TArray Axes; for (const FString& Line : ActionLines) { FInputActionKeyMapping Map; if (FInputActionKeyMapping::StaticStruct()->ImportText(*Line, &Map, nullptr, PPF_None, nullptr, TEXT("ActionMappings")) && Map.Key.IsValid()) { Actions.Add(Map); } } for (const FString& Line : AxisLines) { FInputAxisKeyMapping Map; if (FInputAxisKeyMapping::StaticStruct()->ImportText(*Line, &Map, nullptr, PPF_None, nullptr, TEXT("AxisMappings")) && Map.Key.IsValid()) { Axes.Add(Map); } } UInputSettings* Settings = UInputSettings::GetInputSettings(); if (!Settings || (Actions.Num() == 0 && Axes.Num() == 0)) { UE_LOG(LogLifePlay, Warning, TEXT("Clavier : %s illisible, les touches du jeu sont gardees"), *Path); return false; } // 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 Current = Settings->GetActionMappings(); for (const FInputActionKeyMapping& Map : Current) { Settings->RemoveActionMapping(Map, false); } for (const FInputActionKeyMapping& Map : Actions) { Settings->AddActionMapping(Map, false); } } if (Axes.Num() > 0) { const TArray Current = Settings->GetAxisMappings(); for (const FInputAxisKeyMapping& Map : Current) { Settings->RemoveAxisMapping(Map, false); } for (const FInputAxisKeyMapping& Map : Axes) { Settings->AddAxisMapping(Map, false); } } Settings->ForceRebuildKeymaps(); UE_LOG(LogLifePlay, Display, TEXT("Clavier : %d actions et %d axes relus depuis %s"), Actions.Num(), Axes.Num(), *Path); 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(); } }