diff --git a/.github/workflows/source-regressions.yml b/.github/workflows/source-regressions.yml index bdfa06df..531ca252 100644 --- a/.github/workflows/source-regressions.yml +++ b/.github/workflows/source-regressions.yml @@ -55,6 +55,10 @@ on: - gpu/vk/movie_upscale_policy.h - tests/test_movie_upscale_policy.cpp - tests/test_movie_upscale_policy.py + - host/display.c + - host/posix/display_sdl.c + - host/nn_hid.c + - tests/test_alt_enter.py - nx2/test_import_windows.py - tests/test_handle_lookup.py - builder/linux/hookshot-launch @@ -117,6 +121,10 @@ on: - gpu/vk/movie_upscale_policy.h - tests/test_movie_upscale_policy.cpp - tests/test_movie_upscale_policy.py + - host/display.c + - host/posix/display_sdl.c + - host/nn_hid.c + - tests/test_alt_enter.py - nx2/test_import_windows.py - tests/test_handle_lookup.py - builder/linux/hookshot-launch @@ -227,6 +235,9 @@ jobs: - name: Check movie upscale fallback policy if: runner.os == 'Linux' run: python -I -B tests/test_movie_upscale_policy.py + - name: Check Alt+Enter is fullscreen, not Plus + if: runner.os == 'Linux' + run: python -I -B tests/test_alt_enter.py - name: Check import windows if: runner.os == 'Linux' run: python -I -B nx2/test_import_windows.py --cxx g++ diff --git a/host/display.c b/host/display.c index d419e532..de2b60dd 100644 --- a/host/display.c +++ b/host/display.c @@ -4,7 +4,7 @@ no emulated GPU, so the renderer is the only way to a picture. HOS_HEADLESS=1: no window (scripted runs; the renderer draws offscreen). Closing the window ends the run. HOS_WINDOW_NOACTIVATE=1: the window opens behind the one in front (test runs do not take the keyboard). - HOS_WINDOW_SIZE=x: the client area (default 1280x720 at 96 dpi). F11: fullscreen. + HOS_WINDOW_SIZE=x: the client area (default 1280x720 at 96 dpi). F11 or Alt+Enter: fullscreen. Mouse: a click in the window captures the mouse (hidden, kept in the window), Esc or leaving the window releases it (nn_hid.c reads its motion). */ #include "hos.h" @@ -113,6 +113,15 @@ static LRESULT CALLBACK wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { if (wp == VK_ESCAPE) mouse_capture(w, 0); if (wp == VK_F11 && !(lp & (1 << 30))) set_fullscreen(w, !fullscreen); return 0; + case WM_SYSKEYDOWN: /* Alt+Enter, as on Linux (display_sdl.c); other Alt keys (Alt+F4) go on to Windows */ + if (wp == VK_RETURN && (lp & (1 << 29))) { + if (!(lp & (1 << 30))) set_fullscreen(w, !fullscreen); + return 0; + } + break; + case WM_SYSCHAR: /* (its character: without this Windows beeps, as for an Alt key with no menu) */ + if (wp == '\r') return 0; + break; case WM_APP + 7: /* (the Insert menu's row) */ set_fullscreen(w, (int)wp); return 0; diff --git a/host/nn_hid.c b/host/nn_hid.c index d7ac59cc..b4940a90 100644 --- a/host/nn_hid.c +++ b/host/nn_hid.c @@ -169,6 +169,10 @@ static int mouse_stick(int32_t *rx, int32_t *ry, int pointing) { return 1; } +/* A bound key is down: Enter held with Alt is no button (Alt+Enter is fullscreen, host/display.c and + host/posix/display_sdl.c). tests/test_alt_enter.py */ +static int kmap_down(int vk, int (*down)(int)) { return down(vk) && !(vk == VK_RETURN && down(VK_MENU)); } + static int game_in_front(void) { DWORD pid = 0; HWND w = GetForegroundWindow(); @@ -543,7 +547,7 @@ static void read_input(uint64_t *buttons, int32_t stick[4]) { binds_load(); /* (the keys: key_default's, or hookshot_options.ini's) */ for (int i = 0; i < HOS_SW_N; i++) for (int k = 0; k < 2; k++) - if (key_bind[i][k] && key(key_bind[i][k])) *buttons |= 1ULL << i; + if (key_bind[i][k] && kmap_down(key_bind[i][k], key)) *buttons |= 1ULL << i; int32_t mouse[4] = {0}; /* left button Y (attack), right button ZL (shield, lock on), while the mouse is captured; a button held when the capture began counts only after it was let go (the click that captures never reaches the game) */ diff --git a/tests/test_alt_enter.py b/tests/test_alt_enter.py new file mode 100644 index 00000000..0964d67d --- /dev/null +++ b/tests/test_alt_enter.py @@ -0,0 +1,96 @@ +"""Alt+Enter toggles fullscreen and presses no button (Enter is Plus by default, and any button can be bound to it; issue #390). + +The bound keys' helper (host/nn_hid.c's kmap_down) is compiled alone against fake key states; the window +procedures (host/display.c, host/posix/display_sdl.c) are checked to toggle on Alt+Enter, once per press, and to +leave other Alt keys (Alt+F4) to Windows. + +python -I -B tests/test_alt_enter.py [--cc gcc] +""" +import argparse +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def block(text, marker): + start = text.index(marker) + end = text.index("{", start) + 1 + depth = 1 + while depth: + depth += (text[end] == "{") - (text[end] == "}") + end += 1 + return text[start:end] + + +def case(text, label): + """The body of one `case label:` of a switch, up to the next case""" + start = text.index("case %s:" % label) + end = text.index(" case ", start + 1) + return text[start:end] + + +def check_windows(): + src = (ROOT / "host" / "display.c").read_text(encoding="utf-8") + sys_down = case(src, "WM_SYSKEYDOWN") + for need, why in (("VK_RETURN", "Enter"), ("1 << 29", "the Alt bit"), ("1 << 30", "a held key repeating"), + ("set_fullscreen(w, !fullscreen)", "the toggle"), ("break;", "other Alt keys to DefWindowProc")): + if need not in sys_down: + raise SystemExit("display.c: WM_SYSKEYDOWN lacks %s (%s)" % (need, why)) + if "'\\r'" not in case(src, "WM_SYSCHAR"): + raise SystemExit("display.c: Alt+Enter's WM_SYSCHAR is not taken (Windows beeps)") + + +def check_linux(): + src = (ROOT / "host" / "posix" / "display_sdl.c").read_text(encoding="utf-8") + if "scancode == 40 && (mod & 0x300)" not in src: + raise SystemExit("display_sdl.c: Alt+Enter no longer toggles fullscreen") + + +FIXTURE = r'''#include +#include +#define VK_RETURN 0x0D +#define VK_MENU 0x12 +#define VK_TAB 0x09 +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL:%d %s\n", __LINE__, #x); exit(1); } } while (0) +static int held[256]; +static int down(int vk) { return held[vk & 255]; } +@KMAP_DOWN@ +int main(void) { + held[VK_RETURN] = 1; + CHECK(kmap_down(VK_RETURN, down)); /* Enter alone is Plus */ + held[VK_MENU] = 1; + CHECK(!kmap_down(VK_RETURN, down)); /* Alt+Enter is not */ + held[VK_TAB] = 1; + CHECK(kmap_down(VK_TAB, down)); /* Alt does not hide the other keys */ + held[VK_RETURN] = 0; + CHECK(!kmap_down(VK_RETURN, down)); + held[VK_MENU] = 0; + CHECK(!kmap_down(VK_MENU, down) && kmap_down(VK_TAB, down)); + printf("PASS: Alt+Enter is fullscreen, not Plus\n"); + return 0; +} +''' + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--cc", default="gcc") + a = p.parse_args() + check_windows() + check_linux() + hid = (ROOT / "host" / "nn_hid.c").read_text(encoding="utf-8") + fn = block(hid, "static int kmap_down(") + if "kmap_down(key_bind[i][k], key)" not in hid: + raise SystemExit("nn_hid.c: the bound keys no longer go through kmap_down") + with tempfile.TemporaryDirectory(prefix="alt-enter-") as tmp: + c = Path(tmp) / "fixture.c" + exe = Path(tmp) / "fixture" + c.write_text(FIXTURE.replace("@KMAP_DOWN@", fn), encoding="utf-8") + subprocess.run([a.cc, "-std=c11", "-Wall", "-Wextra", "-Werror", str(c), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + main()