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