Add a Hotkey class for user-configurable keys. Fixes #3779.

Users can now define and use hotkeys that include modifiers (ctrl/meta/shift/alt).
This commit is contained in:
Paul Chote
2013-10-20 11:53:41 +13:00
parent aab6fec68b
commit 7ffbfb9b7e
16 changed files with 297 additions and 52 deletions

View File

@@ -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;

119
OpenRA.FileFormats/Hotkey.cs Executable file
View File

@@ -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;
}
}
}

View File

@@ -16,7 +16,6 @@ namespace OpenRA
{
public enum Keycode
{
UNDEFINED = -1,
UNKNOWN = 0,
FIRST = 0,
BACKSPACE = 8,

View File

@@ -152,6 +152,7 @@
<Compile Include="Graphics\R8Reader.cs" />
<Compile Include="Graphics\TileSetRenderer.cs" />
<Compile Include="Keycode.cs" />
<Compile Include="Hotkey.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">

View File

@@ -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

View File

@@ -236,6 +236,7 @@
<Compile Include="Traits\Player\PlayerHighlightPalette.cs" />
<Compile Include="Traits\World\ScreenMap.cs" />
<Compile Include="Traits\World\ActorMap.cs" />
<Compile Include="Widgets\HotkeyEntryWidget.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenRA.FileFormats\OpenRA.FileFormats.csproj">

View File

@@ -18,8 +18,8 @@ namespace OpenRA.Widgets
{
public class ButtonWidget : Widget
{
public Func<ButtonWidget, string> GetKey = _ => null;
public string Key
public Func<ButtonWidget, Hotkey> 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())

View File

@@ -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<bool> 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); }
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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));

View File

@@ -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;

View File

@@ -335,22 +335,13 @@ namespace OpenRA.Mods.RA.Widgets.Logic
return true;
}
void SetupKeyBinding(ScrollItemWidget keyWidget, string description, Func<string> getValue, Action<string> setValue)
void SetupKeyBinding(ScrollItemWidget keyWidget, string description, Func<Hotkey> getValue, Action<Hotkey> setValue)
{
keyWidget.Get<LabelWidget>("FUNCTION").GetText = () => description;
var textBox = keyWidget.Get<TextFieldWidget>("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<HotkeyEntryWidget>("HOTKEY");
keyEntry.Key = getValue();
keyEntry.OnLoseFocus = () => setValue(keyEntry.Key);
}
static bool ShowRendererDropdown(DropDownButtonWidget dropdown, GraphicSettings s)

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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