Maelstrom: Added a secondary hotkey for buttons

From a73f475bd0d32a3fde14d5745d2c10733fad1a4f Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 21 Mar 2026 10:58:07 -0700
Subject: [PATCH] Added a secondary hotkey for buttons

This allows us to bind the return key to start playing and the escape key to quit.
---
 Data/UI/main.xml              |  4 +-
 screenlib/UIDialogButton.cpp  |  2 +-
 screenlib/UIElementButton.cpp | 99 ++++++++++++++++++++---------------
 screenlib/UIElementButton.h   | 12 +++--
 4 files changed, 70 insertions(+), 47 deletions(-)

diff --git a/Data/UI/main.xml b/Data/UI/main.xml
index 8ff48544..5188d865 100644
--- a/Data/UI/main.xml
+++ b/Data/UI/main.xml
@@ -184,7 +184,7 @@
 		-->
 
 		<!-- Instructions -->
-		<Button name="PlayButton" template="MenuButton" hotkey="P" text="P" action="play" clickSound="114">
+		<Button name="PlayButton" template="MenuButton" hotkey="P" hotkey2="Return" text="P" action="play" clickSound="114">
 			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPRIGHT" anchor="vertical_divider" x="9" y="10"/>
 		</Button>
 		<Label template="SmallYellow" text=" Start playing Maelstrom">
@@ -220,7 +220,7 @@
 		<Label template="SmallYellow" text=" About Maelstrom...">
 			<Anchor anchorFrom="BOTTOMLEFT" anchorTo="TOPRIGHT" anchor="AboutButton" x="3" y="21"/>
 		</Label>
-		<Button condition="QUIT_AVAILABLE" name="QuitButton" template="MenuButton" hotkey="Q" text="Q" action="quit" clickSound="106">
+		<Button condition="QUIT_AVAILABLE" name="QuitButton" template="MenuButton" hotkey="Q" hotkey2="Escape" text="Q" action="quit" clickSound="106">
 			<Anchor anchorFrom="TOPLEFT" anchorTo="TOPLEFT" anchor="PlayButton" y="170"/>
 		</Button>
 		<Label condition="QUIT_AVAILABLE" template="SmallYellow" text=" Quit Maelstrom">
diff --git a/screenlib/UIDialogButton.cpp b/screenlib/UIDialogButton.cpp
index 0f548430..2bb59218 100644
--- a/screenlib/UIDialogButton.cpp
+++ b/screenlib/UIDialogButton.cpp
@@ -46,7 +46,7 @@ UIDialogButton::Load(rapidxml::xml_node<> *node, const UITemplates *templates)
 
 	LoadBool(node, "default", m_default);
 	if (m_default) {
-		m_hotkey = SDLK_RETURN;
+		m_hotkey.key = SDLK_RETURN;
 	}
 
 	LoadBool(node, "closeDialog", m_closeDialog);
diff --git a/screenlib/UIElementButton.cpp b/screenlib/UIElementButton.cpp
index a1b39efe..788641c1 100644
--- a/screenlib/UIElementButton.cpp
+++ b/screenlib/UIElementButton.cpp
@@ -32,8 +32,6 @@ UIElementButton::UIElementButton(UIBaseElement *parent, const char *name, UIDraw
 	// Turn on mouse events at the UIElement level
 	m_mouseEnabled = true;
 
-	m_hotkey = SDLK_UNKNOWN;
-	m_hotkeyMod = SDL_KMOD_NONE;
 	m_pressSound = NULL;
 	m_releaseSound = NULL;
 	m_clickSound = NULL;
@@ -78,34 +76,15 @@ UIElementButton::Load(rapidxml::xml_node<> *node, const UITemplates *templates)
 
 	attr = node->first_attribute("hotkey", 0, false);
 	if (attr) {
-		const char *value = attr->value();
-		const char *hyphen = SDL_strchr(value, '-');
-
-		if (hyphen) {
-			size_t len = size_t(hyphen-value);
-			if (SDL_strncasecmp(value, "ALT", len) == 0) {
-				m_hotkeyMod = SDL_KMOD_ALT;
-			} else if (SDL_strncasecmp(value, "CTRL", len) == 0 ||
-			           SDL_strncasecmp(value, "Control", len) == 0) {
-				m_hotkeyMod = SDL_KMOD_CTRL;
-			} else if (SDL_strncasecmp(value, "SHIFT", len) == 0) {
-				m_hotkeyMod = SDL_KMOD_SHIFT;
-			} else {
-				SetError("Couldn't interpret hotkey value '%s'", value);
-				return false;
-			}
-			value = hyphen+1;
+		if (!ParseHotkey(attr, &m_hotkey)) {
+			return false;
 		}
+	}
 
-		if (strcmp(value, "any") == 0) {
-			/* This will be a catch-all button */
-			m_hotkey = ~0;
-		} else {
-			m_hotkey = SDL_GetKeyFromName(value);
-			if (m_hotkey == SDLK_UNKNOWN) {
-				SetError("Couldn't interpret hotkey value '%s'", value);
-				return false;
-			}
+	attr = node->first_attribute("hotkey2", 0, false);
+	if (attr) {
+		if (!ParseHotkey(attr, &m_hotkey2)) {
+			return false;
 		}
 	}
 
@@ -143,13 +122,51 @@ UIElementButton::Load(rapidxml::xml_node<> *node, const UITemplates *templates)
 }
 
 bool
-UIElementButton::ShouldHandleKey(SDL_Keycode key)
+UIElementButton::ParseHotkey(rapidxml::xml_attribute<> *attr, Hotkey *hotkey)
 {
-	if (key == m_hotkey) {
+	const char *value = attr->value();
+	const char *hyphen = SDL_strchr(value, '-');
+
+	if (hyphen) {
+		size_t len = size_t(hyphen-value);
+		if (SDL_strncasecmp(value, "ALT", len) == 0) {
+			hotkey->mod = SDL_KMOD_ALT;
+		} else if (SDL_strncasecmp(value, "CTRL", len) == 0 ||
+				   SDL_strncasecmp(value, "Control", len) == 0) {
+			hotkey->mod = SDL_KMOD_CTRL;
+		} else if (SDL_strncasecmp(value, "SHIFT", len) == 0) {
+			hotkey->mod = SDL_KMOD_SHIFT;
+		} else {
+			SetError("Couldn't interpret hotkey value '%s'", value);
+			return false;
+		}
+		value = hyphen+1;
+	} else {
+		hotkey->mod = 0;
+	}
+
+	if (strcmp(value, "any") == 0) {
+		/* This will be a catch-all button */
+		hotkey->key = ~0;
+	} else {
+		hotkey->key = SDL_GetKeyFromName(value);
+		if (hotkey->key == SDLK_UNKNOWN) {
+			SetError("Couldn't interpret hotkey value '%s'", value);
+			return false;
+		}
+	}
+	return true;
+}
+
+bool
+UIElementButton::ShouldHandleKey(const SDL_KeyboardEvent *key, Hotkey *hotkey)
+{
+	if (key->key == hotkey->key &&
+	    (!hotkey->mod || (key->mod & hotkey->mod))) {
 		return true;
 	}
-	if (m_hotkey == ~0u) {
-		switch (key) {
+	if (hotkey->key == ~0u) {
+		switch (key->key) {
 			// Ignore modifier keys
 			case SDLK_LSHIFT:
 			case SDLK_RSHIFT:
@@ -171,7 +188,8 @@ bool
 UIElementButton::HandleEvent(const SDL_Event &event)
 {
 	if (event.type == SDL_EVENT_KEY_DOWN &&
-	    ShouldHandleKey(event.key.key)) {
+	    (ShouldHandleKey(&event.key, &m_hotkey) ||
+	     ShouldHandleKey(&event.key, &m_hotkey2))) {
 		if (!m_mousePressed) {
 			m_mousePressed = true;
 			OnMouseDown();
@@ -180,15 +198,14 @@ UIElementButton::HandleEvent(const SDL_Event &event)
 	}
 
 	if (event.type == SDL_EVENT_KEY_UP &&
-	    ShouldHandleKey(event.key.key)) {
-		if (!m_hotkeyMod || (event.key.mod & m_hotkeyMod)) {
-			if (m_mousePressed) {
-				m_mousePressed = false;
-				OnMouseUp();
-			}
-			OnClick();
-			return true;
+	    (ShouldHandleKey(&event.key, &m_hotkey) ||
+	     ShouldHandleKey(&event.key, &m_hotkey2))) {
+		if (m_mousePressed) {
+			m_mousePressed = false;
+			OnMouseUp();
 		}
+		OnClick();
+		return true;
 	}
 
 	return UIElement::HandleEvent(event);
diff --git a/screenlib/UIElementButton.h b/screenlib/UIElementButton.h
index 5bfeff0f..87bdc746 100644
--- a/screenlib/UIElementButton.h
+++ b/screenlib/UIElementButton.h
@@ -51,16 +51,22 @@ DECLARE_TYPESAFE_CLASS(UIElement)
 	}
 
 protected:
+	struct Hotkey {
+		SDL_Keycode key = SDLK_UNKNOWN;
+		int mod = 0;
+	};
+
 	void SetButtonState(BUTTON_STATE state);
 
 	virtual void UpdateDisabledState() override;
 	virtual void OnClick() override;
 
-	bool ShouldHandleKey(SDL_Keycode key);
+	bool ParseHotkey(rapidxml::xml_attribute<> *attr, Hotkey *hotkey);
+	bool ShouldHandleKey(const SDL_KeyboardEvent* key, Hotkey* hotkey);
 
 protected:
-	SDL_Keycode m_hotkey;
-	int m_hotkeyMod;
+	Hotkey m_hotkey;
+	Hotkey m_hotkey2;
 	char *m_pressSound;
 	char *m_releaseSound;
 	char *m_clickSound;