diff --git a/OpenRA.Mods.Common/Widgets/Logic/CommandHistory.cs b/OpenRA.Mods.Common/Widgets/Logic/CommandHistory.cs new file mode 100644 index 0000000000..a888db6304 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/CommandHistory.cs @@ -0,0 +1,89 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; + +namespace OpenRA.Mods.Common.Widgets.Logic +{ + public sealed class CommandHistory + { + const int MaxHistorySize = 50; + static CommandHistory instance; + static readonly object LockObject = new(); + readonly List history = []; + int currentIndex = -1; + + CommandHistory() { } + + public static CommandHistory Instance + { + get + { + if (instance == null) + { + lock (LockObject) + { + instance ??= new CommandHistory(); + } + } + + return instance; + } + } + + public void AddCommand(string command) + { + if (string.IsNullOrWhiteSpace(command) || !command.StartsWith('/')) + return; + + var trimmedCommand = command.Trim(); + history.RemoveAll(c => c == trimmedCommand); + history.Insert(0, trimmedCommand); + if (history.Count > MaxHistorySize) + history.RemoveAt(history.Count - 1); + + currentIndex = -1; + } + + public string GetPrevious() + { + if (history.Count == 0) + return null; + + if (currentIndex < history.Count - 1) + currentIndex++; + + return history[currentIndex]; + } + + public string GetNext() + { + if (history.Count == 0) + return null; + + if (currentIndex > 0) + { + currentIndex--; + return history[currentIndex]; + } + + if (currentIndex == 0) + currentIndex = -1; + + return null; + } + + public void Reset() + { + currentIndex = -1; + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs index a4a36cf50c..073f12e8d3 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs @@ -49,6 +49,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly Dictionary templates = []; readonly TabCompletionLogic tabCompletion = new(); + readonly CommandHistory commandHistory = CommandHistory.Instance; readonly string chatLineSound = ChromeMetrics.Get("ChatLineSound"); @@ -159,11 +160,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic { var text = chatText.Text.Trim(); var from = world.IsReplay ? null : orderManager.LocalClient.Name; + var commandName = text[1..].Split(' ')[0].ToLowerInvariant(); + var isValidCommand = !string.IsNullOrEmpty(commandName) && + chatTraits.OfType().Any(trait => trait.Commands.ContainsKey(commandName)); foreach (var trait in chatTraits) trait.OnChat(from, text); + + if (isValidCommand) + commandHistory.AddCommand(text); } } + commandHistory.Reset(); chatText.Text = ""; if (!isMenuChat) CloseChat(); @@ -191,9 +199,40 @@ namespace OpenRA.Mods.Common.Widgets.Logic else chatText.YieldKeyboardFocus(); + commandHistory.Reset(); return true; }; + chatText.OnArrowUp = _ => + { + var previousCommand = commandHistory.GetPrevious(); + if (previousCommand != null) + { + chatText.Text = previousCommand; + chatText.CursorPosition = chatText.Text.Length; + return true; + } + + return false; + }; + + chatText.OnArrowDown = _ => + { + var nextCommand = commandHistory.GetNext(); + if (nextCommand != null) + { + chatText.Text = nextCommand; + chatText.CursorPosition = chatText.Text.Length; + return true; + } + else + { + chatText.Text = ""; + commandHistory.Reset(); + return true; + } + }; + chatAvailableIn = new CachedTransform(x => FluentProvider.GetMessage(ChatAvailability, "seconds", x)); if (!isMenuChat) @@ -265,6 +304,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic public void OpenChat() { chatText.Text = ""; + commandHistory.Reset(); chatChrome.Visible = true; chatScrollPanel.ScrollToBottom(); if (!chatText.IsDisabled()) diff --git a/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs b/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs index acc8572181..94b2797d4c 100644 --- a/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs +++ b/OpenRA.Mods.Common/Widgets/TextFieldWidget.cs @@ -57,6 +57,8 @@ namespace OpenRA.Mods.Common.Widgets public Func OnEnterKey = _ => false; public Func OnTabKey = _ => false; public Func OnEscKey = _ => false; + public Func OnArrowUp = _ => false; + public Func OnArrowDown = _ => false; public Func OnAltKey = () => false; public Action OnLoseFocus = () => { }; public Action OnTextEdited = () => { }; @@ -282,6 +284,16 @@ namespace OpenRA.Mods.Common.Widgets break; + case Keycode.UP: + if (OnArrowUp(e)) + return true; + break; + + case Keycode.DOWN: + if (OnArrowDown(e)) + return true; + break; + case Keycode.HOME: ResetBlinkCycle(); if (e.Modifiers.HasModifier(Modifiers.Shift))