Skip to content

Commit 1c6d28d

Browse files
authored
Improved menu keyboard navigation (#4480)
* Block inputs when menus are open in MainForm by moving TAStudio's logic for that into FormBase. * Remove unneeded condition and combine methods into one properly named one. * Fix: Pressing space while the tool strip menu was focused did not work. * Fix: Alt+F did not work for input-blocking tools. * Fix: Pressing alt did not focus the tool stip menu; pressing Alt then F did not work. * Do not focus the tool strip menu if a hotkey or input was done.
1 parent c5938ad commit 1c6d28d

File tree

6 files changed

+63
-48
lines changed

6 files changed

+63
-48
lines changed

src/BizHawk.Client.Common/inputAdapters/InputManager.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,11 @@ private static AutofireController BindToDefinitionAF(
149149
/// <summary>
150150
/// Processes queued inputs and triggers input evets (i.e. hotkeys), but does not update output controllers.<br/>
151151
/// </summary>
152-
/// <param name="processUnboundInput">Events that did not do anything are forwarded out here.
152+
/// <param name="processSpecialInput">All input events are forwarded out here.
153153
/// This allows things like Windows' standard alt hotkeys (for menu items) to be handled by the
154-
/// caller if the input didn't alrady do something else.</param>
155-
public void ProcessInput(IPhysicalInputSource source, Func<string, bool> processHotkey, Config config, Action<InputEvent> processUnboundInput)
154+
/// caller if the input didn't alrady do something else.
155+
/// <br/>The second parameter is true if the input already did something (hotkey or controller input).</param>
156+
public void ProcessInput(IPhysicalInputSource source, Func<string, bool> processHotkey, Config config, Action<InputEvent, bool> processSpecialInput)
156157
{
157158
// loop through all available events
158159
InputEvent ie;
@@ -191,10 +192,7 @@ public void ProcessInput(IPhysicalInputSource source, Func<string, bool> process
191192
}
192193
bool didEmuInput = shouldDoEmuInput && isEmuInput;
193194

194-
if (!didHotkey && !didEmuInput)
195-
{
196-
processUnboundInput(ie);
197-
}
195+
processSpecialInput(ie, didHotkey | didEmuInput);
198196
} // foreach event
199197

200198
// also handle axes
@@ -223,7 +221,7 @@ public void ProcessInput(IPhysicalInputSource source, Func<string, bool> process
223221
}
224222

225223
/// <summary>
226-
/// Update output controllers. Call <see cref="ProcessInput(IPhysicalInputSource, Func{string, bool}, Config, Action{InputEvent})"/> shortly before this.
224+
/// Update output controllers. Call <see cref="ProcessInput(IPhysicalInputSource, Func{string, bool}, Config, Action{InputEvent, bool})"/> shortly before this.
227225
/// </summary>
228226
public void RunControllerChain(Config config)
229227
{

src/BizHawk.Client.EmuHawk/FormBase.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public static void FixBackColorOnControls(Control control)
4646
public virtual bool BlocksInputWhenFocused
4747
=> true;
4848

49+
public bool MenuIsOpen { get; private set; }
50+
4951
public Config? Config { get; set; }
5052

5153
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
@@ -90,6 +92,12 @@ protected override void OnLoad(EventArgs e)
9092
}
9193
if (OSTailoredCode.IsUnixHost) FixBackColorOnControls(this);
9294
UpdateWindowTitle();
95+
96+
if (MainMenuStrip != null)
97+
{
98+
MainMenuStrip.MenuActivate += (_, _) => MenuIsOpen = true;
99+
MainMenuStrip.MenuDeactivate += (_, _) => MenuIsOpen = false;
100+
}
93101
}
94102

95103
public void UpdateWindowTitle()
@@ -98,25 +106,29 @@ public void UpdateWindowTitle()
98106
: WindowTitle;
99107

100108
// Alt key hacks. We need this in order for hotkey bindings with alt to work.
109+
private const int WM_SYSCOMMAND = 0x0112;
110+
private const int SC_KEYMENU = 0xF100;
111+
internal void SendAltCombination(char character)
112+
{
113+
var m = new Message { WParam = new IntPtr(SC_KEYMENU), LParam = new IntPtr(character), Msg = WM_SYSCOMMAND, HWnd = Handle };
114+
if (character == ' ') base.WndProc(ref m);
115+
else if (character >= 'a' && character <= 'z') base.ProcessDialogChar(character);
116+
}
101117

102-
/// <summary>sends a simulation of a plain alt key keystroke</summary>
103-
internal void SendPlainAltKey(int lparam)
118+
internal void FocusToolStipMenu()
104119
{
105-
var m = new Message { WParam = new IntPtr(0xF100), LParam = new IntPtr(lparam), Msg = 0x0112, HWnd = Handle };
120+
var m = new Message { WParam = new IntPtr(SC_KEYMENU), LParam = new IntPtr(0), Msg = WM_SYSCOMMAND, HWnd = Handle };
106121
base.WndProc(ref m);
107122
}
108123

109-
/// <summary>HACK to send an alt+mnemonic combination</summary>
110-
internal void SendAltKeyChar(char c) => ProcessMnemonic(c);
111-
112124
protected override void WndProc(ref Message m)
113125
{
114126
if (!BlocksInputWhenFocused)
115127
{
116128
// this is necessary to trap plain alt keypresses so that only our hotkey system gets them
117-
if (m.Msg == 0x0112) // WM_SYSCOMMAND
129+
if (m.Msg == WM_SYSCOMMAND)
118130
{
119-
if (m.WParam.ToInt32() == 0xF100) // SC_KEYMENU
131+
if (m.WParam.ToInt32() == SC_KEYMENU && m.LParam == IntPtr.Zero)
120132
{
121133
return;
122134
}
@@ -128,6 +140,7 @@ protected override void WndProc(ref Message m)
128140

129141
protected override bool ProcessDialogChar(char charCode)
130142
{
143+
if (BlocksInputWhenFocused) return base.ProcessDialogChar(charCode);
131144
// this is necessary to trap alt+char combinations so that only our hotkey system gets them
132145
return (ModifierKeys & Keys.Alt) != 0 || base.ProcessDialogChar(charCode);
133146
}

src/BizHawk.Client.EmuHawk/MainForm.cs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ _argParser.SocketAddress is var (socketIP, socketPort)
641641
? AllowInput.OnlyController
642642
: AllowInput.All
643643
: AllowInput.None,
644-
FormBase { BlocksInputWhenFocused: false } => AllowInput.All,
644+
FormBase { BlocksInputWhenFocused: false, MenuIsOpen: false } => AllowInput.All,
645645
ControllerConfig => AllowInput.All,
646646
HotkeyConfig => AllowInput.All,
647647
LuaWinform { BlocksInputWhenFocused: false } => AllowInput.All,
@@ -899,6 +899,13 @@ private void CheckMayCloseAndCleanup(object/*?*/ closingSender, CancelEventArgs
899899

900900
public override bool BlocksInputWhenFocused { get; } = false;
901901

902+
/// <summary>
903+
/// Windows does tool stip menu focus things when Alt is released, not pressed.
904+
/// However, if an alt combination is pressed then those things happen at that time instead.
905+
/// So we need to know if a key combination was used, so we can skip the alt release logic.
906+
/// </summary>
907+
private bool _skipNextAltRelease = true;
908+
902909
public int ProgramRunLoop()
903910
{
904911
// needs to be done late, after the log console snaps on top
@@ -929,26 +936,40 @@ public int ProgramRunLoop()
929936
InputManager.ActiveController.PrepareHapticsForHost(finalHostController);
930937
Input.Instance.Adapter.SetHaptics(finalHostController.GetHapticsSnapshot());
931938

932-
InputManager.ProcessInput(Input.Instance, CheckHotkey, Config, (ie) =>
939+
InputManager.ProcessInput(Input.Instance, CheckHotkey, Config, (ie, handled) =>
933940
{
934941
if (ActiveForm is not FormBase afb) return;
935942

936943
// Alt key for menu items.
937944
if (ie.EventType is InputEventType.Press && (ie.LogicalButton.Modifiers & LogicalButton.MASK_ALT) is not 0U)
938945
{
946+
// Windows will not focus the menu if any other key was pressed while Alt is held. Regardless of whether that key did anything.
947+
_skipNextAltRelease = true;
948+
if (handled) return;
949+
939950
if (ie.LogicalButton.Button.Length == 1)
940951
{
941952
var c = ie.LogicalButton.Button.ToLowerInvariant()[0];
942-
if ((c >= 'a' && c <= 'z') || c == ' ')
943-
{
944-
afb.SendAltKeyChar(c);
945-
}
953+
afb.SendAltCombination(c);
946954
}
947955
else if (ie.LogicalButton.Button == "Space")
948956
{
949-
afb.SendPlainAltKey(32);
957+
afb.SendAltCombination(' ');
950958
}
951959
}
960+
else if (handled) return;
961+
else if (ie.EventType is InputEventType.Press && ie.LogicalButton.Button == "Alt")
962+
{
963+
// We will only do the alt release if the alt press itself was not already handled.
964+
_skipNextAltRelease = false;
965+
}
966+
else if (ie.EventType is InputEventType.Release
967+
&& !afb.BlocksInputWhenFocused
968+
&& ie.LogicalButton.Button == "Alt"
969+
&& !_skipNextAltRelease)
970+
{
971+
afb.FocusToolStipMenu();
972+
}
952973

953974
// same as right-click
954975
if (ie.ToString() == "Press:Apps" && Config.ShowContextMenu && ContainsFocus)

src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Designer.cs

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public partial class TAStudio : ToolFormBase, IToolFormAutoConfig, IControlMainf
2020
public static Icon ToolIcon
2121
=> Resources.TAStudioIcon;
2222

23-
public override bool BlocksInputWhenFocused => IsInMenuLoop;
23+
public override bool BlocksInputWhenFocused => false;
2424

2525
public new IMainFormForTools MainForm => base.MainForm;
2626

@@ -29,8 +29,6 @@ public static Icon ToolIcon
2929
// TODO: UI flow that conveniently allows to start from savestate
3030
public ITasMovie CurrentTasMovie => MovieSession.Movie as ITasMovie;
3131

32-
public bool IsInMenuLoop { get; private set; }
33-
3432
private readonly List<TasClipboardEntry> _tasClipboard = new List<TasClipboardEntry>();
3533
private const string CursorColumnName = "CursorColumn";
3634
private const string FrameColumnName = "FrameColumn";
@@ -1093,16 +1091,6 @@ private void TasView_CellDropped(object sender, InputRoll.CellEventArgs e)
10931091
}
10941092
}
10951093

1096-
private void TASMenu_MenuActivate(object sender, EventArgs e)
1097-
{
1098-
IsInMenuLoop = true;
1099-
}
1100-
1101-
private void TASMenu_MenuDeactivate(object sender, EventArgs e)
1102-
{
1103-
IsInMenuLoop = false;
1104-
}
1105-
11061094
// Stupid designer
11071095
protected void DragEnterWrapper(object sender, DragEventArgs e)
11081096
{

src/BizHawk.Tests.Client.Common/InputManagerTests.cs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void EmulateFrameAdvance(bool lag = false)
4848

4949
public void BasicInputProcessing()
5050
{
51-
manager.ProcessInput(source, ProcessHotkey, config, (_) => { });
51+
manager.ProcessInput(source, ProcessHotkey, config, (_, _) => { });
5252
manager.RunControllerChain(config);
5353
}
5454

@@ -448,42 +448,39 @@ public void AutofireHotkeyDoesNotRespondToAlreadyHeldButton()
448448
}
449449

450450
[TestMethod]
451-
public void HotkeyIsNotSeenAsUnbound()
451+
public void HotkeyIsNotSeenAsUnhandled()
452452
{
453453
Context context = new(_hotkeys);
454454
InputManager manager = context.manager;
455455
FakeInputSource source = context.source;
456456
manager.ClientControls.BindMulti(_hotkeys[0], "Q");
457457

458458
source.MakePressEvent("Q");
459-
manager.ProcessInput(source, context.ProcessHotkey, context.config, (_) => Assert.Fail("Bound key was seen as unbound."));
459+
manager.ProcessInput(source, context.ProcessHotkey, context.config, (_, handled) => Assert.IsTrue(handled, "Bound key was seen as unbound."));
460460
}
461461

462462
[TestMethod]
463-
public void InputIsNotSeenAsUnbound()
463+
public void InputIsNotSeenAsUnhandled()
464464
{
465465
Context context = new(_hotkeys);
466466
InputManager manager = context.manager;
467467
FakeInputSource source = context.source;
468468
manager.ActiveController.BindMulti("A", "Q");
469469

470470
source.MakePressEvent("Q");
471-
manager.ProcessInput(source, context.ProcessHotkey, context.config, (_) => Assert.Fail("Bound key was seen as unbound."));
471+
manager.ProcessInput(source, context.ProcessHotkey, context.config, (_, handled) => Assert.IsTrue(handled, "Bound key was seen as unbound."));
472472
}
473473

474474
[TestMethod]
475-
public void UnboundInputIsSeen()
475+
public void UnboundInputIsSeenAsUnhandled()
476476
{
477477
Context context = new(_hotkeys);
478478
InputManager manager = context.manager;
479479
FakeInputSource source = context.source;
480480

481481
source.MakePressEvent("A");
482482

483-
bool sawUnboundInput = false;
484-
manager.ProcessInput(source, context.ProcessHotkey, context.config, (_) => sawUnboundInput = true);
485-
486-
Assert.IsTrue(sawUnboundInput);
483+
manager.ProcessInput(source, context.ProcessHotkey, context.config, (_, handled) => Assert.IsFalse(handled, "Unbound key was seen as handled."));
487484
}
488485

489486
[TestMethod]

0 commit comments

Comments
 (0)