From 7ffbfb9b7e299996772f84329bff7ebf25309a05 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 20 Oct 2013 11:53:41 +1300 Subject: [PATCH] Add a Hotkey class for user-configurable keys. Fixes #3779. Users can now define and use hotkeys that include modifiers (ctrl/meta/shift/alt). --- OpenRA.FileFormats/FieldLoader.cs | 9 ++ OpenRA.FileFormats/Hotkey.cs | 119 +++++++++++++++++ OpenRA.FileFormats/Keycode.cs | 1 - OpenRA.FileFormats/OpenRA.FileFormats.csproj | 1 + OpenRA.Game/GameRules/Settings.cs | 28 ++-- OpenRA.Game/OpenRA.Game.csproj | 1 + OpenRA.Game/Widgets/ButtonWidget.cs | 6 +- OpenRA.Game/Widgets/HotkeyEntryWidget.cs | 122 ++++++++++++++++++ .../WorldInteractionControllerWidget.cs | 2 +- .../Widgets/Logic/ButtonTooltipLogic.cs | 2 +- .../Widgets/ProductionTabsWidget.cs | 6 +- OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs | 6 +- .../Widgets/Logic/SettingsMenuLogic.cs | 17 +-- OpenRA.Mods.RA/Widgets/OrderButtonWidget.cs | 2 +- OpenRA.Mods.RA/Widgets/WorldCommandWidget.cs | 21 +-- mods/ra/chrome/settings.yaml | 6 +- 16 files changed, 297 insertions(+), 52 deletions(-) create mode 100755 OpenRA.FileFormats/Hotkey.cs create mode 100644 OpenRA.Game/Widgets/HotkeyEntryWidget.cs diff --git a/OpenRA.FileFormats/FieldLoader.cs b/OpenRA.FileFormats/FieldLoader.cs index 0c4a56b27c..fc64c526f3 100755 --- a/OpenRA.FileFormats/FieldLoader.cs +++ b/OpenRA.FileFormats/FieldLoader.cs @@ -174,6 +174,15 @@ namespace OpenRA.FileFormats return InvalidValueAction(value, fieldType, fieldName); } + else if (fieldType == typeof(Hotkey)) + { + Hotkey res; + if (Hotkey.TryParse(value, out res)) + return res; + + return InvalidValueAction(value, fieldType, fieldName); + } + else if (fieldType == typeof(WRange)) { WRange res; diff --git a/OpenRA.FileFormats/Hotkey.cs b/OpenRA.FileFormats/Hotkey.cs new file mode 100755 index 0000000000..2bbced90f5 --- /dev/null +++ b/OpenRA.FileFormats/Hotkey.cs @@ -0,0 +1,119 @@ +#region Copyright & License Information +/* + * Copyright 2007-2013 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.FileFormats; + +namespace OpenRA +{ + public struct Hotkey + { + public static Hotkey Invalid = new Hotkey(Keycode.UNKNOWN, Modifiers.None); + + public readonly Keycode Key; + public readonly Modifiers Modifiers; + + public static bool TryParse(string s, out Hotkey result) + { + result = Invalid; + + var parts = s.Split(' '); + if (parts.Length >= 2) + { + if (!Enum.GetNames(typeof(Keycode)).Contains(parts[0])) + return false; + + var modString = s.Substring(s.IndexOf(' ')); + var modParts = modString.Split(',').Select(x => x.Trim()); + if (modParts.Any(p => !Enum.GetNames(typeof(Modifiers)).Contains(p))) + return false; + + var key = (Keycode)Enum.Parse(typeof(Keycode), parts[0]); + var mods = (Modifiers)Enum.Parse(typeof(Modifiers), modString); + + result = new Hotkey(key, mods); + return true; + } + + if (parts.Length == 1) + { + // HACK: Try parsing as a legacy key name + // This is a stop-gap solution to keep backwards + // compatibility while outside code is converted + var i = 0; + for (; i < (int)Keycode.LAST; i++) + if (KeycodeExts.DisplayString((Keycode)i) == parts[0]) + break; + + if (i < (int)Keycode.LAST) + { + result = new Hotkey((Keycode)i, Modifiers.None); + return true; + } + } + + return false; + } + + public static Hotkey FromKeyInput(KeyInput ki) + { + return new Hotkey(ki.Key, ki.Modifiers); + } + + public Hotkey(Keycode virtKey, Modifiers mod) + { + Key = virtKey; + Modifiers = mod; + } + + public static bool operator !=(Hotkey a, Hotkey b) { return !(a == b); } + public static bool operator ==(Hotkey a, Hotkey b) + { + // Unknown keys are never equal + if (a.Key == Keycode.UNKNOWN) + return false; + + return a.Key == b.Key && a.Modifiers == b.Modifiers; + } + + public override int GetHashCode() { return Key.GetHashCode() ^ Modifiers.GetHashCode(); } + + public override bool Equals(object obj) + { + if (obj == null) + return false; + + return (Hotkey)obj == this; + } + + public override string ToString() { return "{0} {1}".F(Key, Modifiers.ToString("F")); } + + public string DisplayString() + { + var ret = KeycodeExts.DisplayString(Key).ToUpper(); + + if (Modifiers.HasModifier(Modifiers.Shift)) + ret = "Shift + " + ret; + + if (Modifiers.HasModifier(Modifiers.Alt)) + ret = "Alt + " + ret; + + if (Modifiers.HasModifier(Modifiers.Ctrl)) + ret = "Ctrl + " + ret; + + if (Modifiers.HasModifier(Modifiers.Meta)) + ret = (Platform.CurrentPlatform == PlatformType.OSX ? "Cmd + " : "Meta + ") + ret; + + return ret; + } + } +} diff --git a/OpenRA.FileFormats/Keycode.cs b/OpenRA.FileFormats/Keycode.cs index 2173a2944a..bba38dbf8e 100755 --- a/OpenRA.FileFormats/Keycode.cs +++ b/OpenRA.FileFormats/Keycode.cs @@ -16,7 +16,6 @@ namespace OpenRA { public enum Keycode { - UNDEFINED = -1, UNKNOWN = 0, FIRST = 0, BACKSPACE = 8, diff --git a/OpenRA.FileFormats/OpenRA.FileFormats.csproj b/OpenRA.FileFormats/OpenRA.FileFormats.csproj index 723220ae63..ecdf6dfe9b 100644 --- a/OpenRA.FileFormats/OpenRA.FileFormats.csproj +++ b/OpenRA.FileFormats/OpenRA.FileFormats.csproj @@ -152,6 +152,7 @@ + diff --git a/OpenRA.Game/GameRules/Settings.cs b/OpenRA.Game/GameRules/Settings.cs index 0d9f81578c..4000f15f1f 100644 --- a/OpenRA.Game/GameRules/Settings.cs +++ b/OpenRA.Game/GameRules/Settings.cs @@ -148,23 +148,23 @@ namespace OpenRA.GameRules public class KeySettings { - public string CycleBaseKey = "backspace"; - public string ToLastEventKey = "space"; - public string ToSelectionKey = "home"; + public Hotkey CycleBaseKey = new Hotkey(Keycode.BACKSPACE, Modifiers.None); + public Hotkey ToLastEventKey = new Hotkey(Keycode.SPACE, Modifiers.None); + public Hotkey ToSelectionKey = new Hotkey(Keycode.HOME, Modifiers.None); - public string PauseKey = "f9"; - public string SellKey = "f10"; - public string PowerDownKey = "f11"; - public string RepairKey = "f12"; + public Hotkey PauseKey = new Hotkey(Keycode.F9, Modifiers.None); + public Hotkey SellKey = new Hotkey(Keycode.F10, Modifiers.None); + public Hotkey PowerDownKey = new Hotkey(Keycode.F11, Modifiers.None); + public Hotkey RepairKey = new Hotkey(Keycode.F12, Modifiers.None); - public string AttackMoveKey = "a"; - public string StopKey = "s"; - public string ScatterKey = "x"; - public string DeployKey = "f"; - public string StanceCycleKey = "z"; - public string GuardKey = "d"; + public Hotkey AttackMoveKey = new Hotkey(Keycode.A, Modifiers.None); + public Hotkey StopKey = new Hotkey(Keycode.S, Modifiers.None); + public Hotkey ScatterKey = new Hotkey(Keycode.X, Modifiers.None); + public Hotkey DeployKey = new Hotkey(Keycode.F, Modifiers.None); + public Hotkey StanceCycleKey = new Hotkey(Keycode.Z, Modifiers.None); + public Hotkey GuardKey = new Hotkey(Keycode.D, Modifiers.None); - public string CycleTabsKey = "tab"; + public Hotkey CycleTabsKey = new Hotkey(Keycode.TAB, Modifiers.None); } public class IrcSettings diff --git a/OpenRA.Game/OpenRA.Game.csproj b/OpenRA.Game/OpenRA.Game.csproj index eca34a8364..a60f54b7d9 100644 --- a/OpenRA.Game/OpenRA.Game.csproj +++ b/OpenRA.Game/OpenRA.Game.csproj @@ -236,6 +236,7 @@ + diff --git a/OpenRA.Game/Widgets/ButtonWidget.cs b/OpenRA.Game/Widgets/ButtonWidget.cs index 3166571476..a14d548487 100644 --- a/OpenRA.Game/Widgets/ButtonWidget.cs +++ b/OpenRA.Game/Widgets/ButtonWidget.cs @@ -18,8 +18,8 @@ namespace OpenRA.Widgets { public class ButtonWidget : Widget { - public Func GetKey = _ => null; - public string Key + public Func GetKey = _ => Hotkey.Invalid; + public Hotkey Key { get { return GetKey(this); } set { GetKey = _ => value; } @@ -91,7 +91,7 @@ namespace OpenRA.Widgets public override bool HandleKeyPress(KeyInput e) { - if (KeycodeExts.DisplayString(e.Key) != Key || e.Event != KeyInputEvent.Down) + if (Hotkey.FromKeyInput(e) != Key || e.Event != KeyInputEvent.Down) return false; if (!IsDisabled()) diff --git a/OpenRA.Game/Widgets/HotkeyEntryWidget.cs b/OpenRA.Game/Widgets/HotkeyEntryWidget.cs new file mode 100644 index 0000000000..714b3b1da1 --- /dev/null +++ b/OpenRA.Game/Widgets/HotkeyEntryWidget.cs @@ -0,0 +1,122 @@ +#region Copyright & License Information +/* + * Copyright 2007-2013 The OpenRA Developers (see AUTHORS) + * 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Drawing; +using System.Linq; +using OpenRA.Traits; +using OpenRA.Graphics; + +namespace OpenRA.Widgets +{ + public class HotkeyEntryWidget : Widget + { + public Hotkey Key; + + public int VisualHeight = 1; + public int LeftMargin = 5; + public int RightMargin = 5; + + public Action OnLoseFocus = () => { }; + + public Func IsDisabled = () => false; + public Color TextColor = Color.White; + public Color DisabledColor = Color.Gray; + public string Font = "Regular"; + + public HotkeyEntryWidget() : base() {} + protected HotkeyEntryWidget(HotkeyEntryWidget widget) + : base(widget) + { + Font = widget.Font; + TextColor = widget.TextColor; + DisabledColor = widget.DisabledColor; + VisualHeight = widget.VisualHeight; + } + + public override bool YieldKeyboardFocus() + { + OnLoseFocus(); + return base.YieldKeyboardFocus(); + } + + public override bool HandleMouseInput(MouseInput mi) + { + if (IsDisabled()) + return false; + + if (mi.Event != MouseInputEvent.Down) + return false; + + // Attempt to take keyboard focus + if (!RenderBounds.Contains(mi.Location) || !TakeKeyboardFocus()) + return false; + + return true; + } + + static readonly Keycode[] IgnoreKeys = new Keycode[] + { + Keycode.RSHIFT, Keycode.LSHIFT, + Keycode.RCTRL, Keycode.LCTRL, + Keycode.RALT, Keycode.LALT, + Keycode.RMETA, Keycode.LMETA + }; + + public override bool HandleKeyPress(KeyInput e) + { + if (IsDisabled() || e.Event == KeyInputEvent.Up) + return false; + + if (!HasKeyboardFocus || IgnoreKeys.Contains(e.Key)) + return false; + + Key = Hotkey.FromKeyInput(e); + + return true; + } + + public override void Draw() + { + var apparentText = Key.DisplayString(); + + var font = Game.Renderer.Fonts[Font]; + var pos = RenderOrigin; + + var textSize = font.Measure(apparentText); + + var disabled = IsDisabled(); + var state = disabled ? "textfield-disabled" : + HasKeyboardFocus ? "textfield-focused" : + Ui.MouseOverWidget == this ? "textfield-hover" : + "textfield"; + + WidgetUtils.DrawPanel(state, RenderBounds); + + // Inset text by the margin and center vertically + var textPos = pos + new int2(LeftMargin, (Bounds.Height - textSize.Y) / 2 - VisualHeight); + + // Scissor when the text overflows + if (textSize.X > Bounds.Width - LeftMargin - RightMargin) + { + Game.Renderer.EnableScissor(pos.X + LeftMargin, pos.Y, + Bounds.Width - LeftMargin - RightMargin, Bounds.Bottom); + } + + var color = disabled ? DisabledColor : TextColor; + font.DrawText(apparentText, textPos, color); + + if (textSize.X > Bounds.Width - LeftMargin - RightMargin) + Game.Renderer.DisableScissor(); + } + + public override Widget Clone() { return new HotkeyEntryWidget(this); } + } +} \ No newline at end of file diff --git a/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs b/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs index c60daa2fbe..27fb1e0b60 100644 --- a/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs +++ b/OpenRA.Game/Widgets/WorldInteractionControllerWidget.cs @@ -177,7 +177,7 @@ namespace OpenRA.Widgets } // Disable pausing for spectators - else if (KeycodeExts.DisplayString(e.Key) == Game.Settings.Keys.PauseKey && world.LocalPlayer != null) + else if (Hotkey.FromKeyInput(e) == Game.Settings.Keys.PauseKey && world.LocalPlayer != null) world.SetPauseState(!world.Paused); } return false; diff --git a/OpenRA.Mods.Cnc/Widgets/Logic/ButtonTooltipLogic.cs b/OpenRA.Mods.Cnc/Widgets/Logic/ButtonTooltipLogic.cs index 713bdc578b..49277b0643 100644 --- a/OpenRA.Mods.Cnc/Widgets/Logic/ButtonTooltipLogic.cs +++ b/OpenRA.Mods.Cnc/Widgets/Logic/ButtonTooltipLogic.cs @@ -24,7 +24,7 @@ namespace OpenRA.Mods.Cnc.Widgets.Logic var labelWidth = Game.Renderer.Fonts[label.Font].Measure(button.TooltipText).X; label.Bounds.Width = labelWidth; - var hotkeyLabel = "({0})".F(button.Key.ToUpperInvariant()); + var hotkeyLabel = "({0})".F(button.Key.DisplayString()); hotkey.GetText = () => hotkeyLabel; hotkey.Bounds.X = labelWidth + 2 * label.Bounds.X; diff --git a/OpenRA.Mods.Cnc/Widgets/ProductionTabsWidget.cs b/OpenRA.Mods.Cnc/Widgets/ProductionTabsWidget.cs index 3c9621e821..336b9fa97e 100755 --- a/OpenRA.Mods.Cnc/Widgets/ProductionTabsWidget.cs +++ b/OpenRA.Mods.Cnc/Widgets/ProductionTabsWidget.cs @@ -274,8 +274,10 @@ namespace OpenRA.Mods.Cnc.Widgets public override bool HandleKeyPress(KeyInput e) { - if (e.Event != KeyInputEvent.Down) return false; - if (KeycodeExts.DisplayString(e.Key) == Game.Settings.Keys.CycleTabsKey) + if (e.Event != KeyInputEvent.Down) + return false; + + if (Hotkey.FromKeyInput(e) == Game.Settings.Keys.CycleTabsKey) { Sound.PlayNotification(null, "Sounds", "ClickSound", null); SelectNextTab(e.Modifiers.HasModifier(Modifiers.Shift)); diff --git a/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs b/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs index 558ba35d55..2f658a89b0 100755 --- a/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs +++ b/OpenRA.Mods.RA/Widgets/BuildPaletteWidget.cs @@ -146,8 +146,10 @@ namespace OpenRA.Mods.RA.Widgets public override bool HandleKeyPress(KeyInput e) { - if (e.Event == KeyInputEvent.Up) return false; - if (KeycodeExts.DisplayString(e.Key) == Game.Settings.Keys.CycleTabsKey) + if (e.Event == KeyInputEvent.Up) + return false; + + if (Hotkey.FromKeyInput(e) == Game.Settings.Keys.CycleTabsKey) { TabChange(e.Modifiers.HasModifier(Modifiers.Shift)); return true; diff --git a/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs b/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs index 97793acf60..aedc3103d3 100644 --- a/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs +++ b/OpenRA.Mods.RA/Widgets/Logic/SettingsMenuLogic.cs @@ -335,22 +335,13 @@ namespace OpenRA.Mods.RA.Widgets.Logic return true; } - void SetupKeyBinding(ScrollItemWidget keyWidget, string description, Func getValue, Action setValue) + void SetupKeyBinding(ScrollItemWidget keyWidget, string description, Func getValue, Action setValue) { keyWidget.Get("FUNCTION").GetText = () => description; - var textBox = keyWidget.Get("HOTKEY"); - - textBox.Text = getValue(); - textBox.OnLoseFocus = () => - { - textBox.Text.Trim(); - if (textBox.Text.Length == 0) - textBox.Text = getValue(); - else - setValue(textBox.Text); - }; - textBox.OnEnterKey = () => { textBox.YieldKeyboardFocus(); return true; }; + var keyEntry = keyWidget.Get("HOTKEY"); + keyEntry.Key = getValue(); + keyEntry.OnLoseFocus = () => setValue(keyEntry.Key); } static bool ShowRendererDropdown(DropDownButtonWidget dropdown, GraphicSettings s) diff --git a/OpenRA.Mods.RA/Widgets/OrderButtonWidget.cs b/OpenRA.Mods.RA/Widgets/OrderButtonWidget.cs index ecb58a2817..386681042d 100755 --- a/OpenRA.Mods.RA/Widgets/OrderButtonWidget.cs +++ b/OpenRA.Mods.RA/Widgets/OrderButtonWidget.cs @@ -27,7 +27,7 @@ namespace OpenRA.Mods.RA.Widgets public OrderButtonWidget() { GetImage = () => Enabled() ? Pressed() ? "pressed" : "normal" : "disabled"; - GetDescription = () => Key != null ? "{0} ({1})".F(Description, Key.ToUpper()) : Description; + GetDescription = () => Key != Hotkey.Invalid ? "{0} ({1})".F(Description, Key.DisplayString()) : Description; GetLongDesc = () => LongDesc; } diff --git a/OpenRA.Mods.RA/Widgets/WorldCommandWidget.cs b/OpenRA.Mods.RA/Widgets/WorldCommandWidget.cs index e4188c9a1a..d6ec34dd07 100644 --- a/OpenRA.Mods.RA/Widgets/WorldCommandWidget.cs +++ b/OpenRA.Mods.RA/Widgets/WorldCommandWidget.cs @@ -45,38 +45,39 @@ namespace OpenRA.Mods.RA.Widgets bool ProcessInput(KeyInput e) { - if (e.Modifiers == Modifiers.None && e.Event == KeyInputEvent.Down) + if (e.Event == KeyInputEvent.Down) { + var key = Hotkey.FromKeyInput(e); var ks = Game.Settings.Keys; - if (KeycodeExts.DisplayString(e.Key) == ks.CycleBaseKey) + if (key == ks.CycleBaseKey) return CycleBases(); - if (KeycodeExts.DisplayString(e.Key) == ks.ToLastEventKey) + if (key == ks.ToLastEventKey) return ToLastEvent(); - if (KeycodeExts.DisplayString(e.Key) == ks.ToSelectionKey) + if (key == ks.ToSelectionKey) return ToSelection(); // Put all functions that aren't unit-specific before this line! if (!world.Selection.Actors.Any()) return false; - if (KeycodeExts.DisplayString(e.Key) == ks.AttackMoveKey) + if (key == ks.AttackMoveKey) return PerformAttackMove(); - if (KeycodeExts.DisplayString(e.Key) == ks.StopKey) + if (key == ks.StopKey) return PerformStop(); - if (KeycodeExts.DisplayString(e.Key) == ks.ScatterKey) + if (key == ks.ScatterKey) return PerformScatter(); - if (KeycodeExts.DisplayString(e.Key) == ks.DeployKey) + if (key == ks.DeployKey) return PerformDeploy(); - if (KeycodeExts.DisplayString(e.Key) == ks.StanceCycleKey) + if (key == ks.StanceCycleKey) return PerformStanceCycle(); - if (KeycodeExts.DisplayString(e.Key) == ks.GuardKey) + if (key == ks.GuardKey) return PerformGuard(); } diff --git a/mods/ra/chrome/settings.yaml b/mods/ra/chrome/settings.yaml index 6c4ed0af40..84056ab4df 100644 --- a/mods/ra/chrome/settings.yaml +++ b/mods/ra/chrome/settings.yaml @@ -321,11 +321,10 @@ Background@SETTINGS_MENU: X:10 Width:200 Height:25 - TextField@HOTKEY: + HotkeyEntry@HOTKEY: X:250 Width:139 Height:25 - MaxLength:16 Label@KEYS_UNITCOMMANDSHEADLINE: X:0 Y:130 @@ -348,11 +347,10 @@ Background@SETTINGS_MENU: X:10 Width:200 Height:25 - TextField@HOTKEY: + HotkeyEntry@HOTKEY: X:250 Width:139 Height:25 - MaxLength:16 Container@DEBUG_PANE: X:37 Y:100