From 66d00ec76fad62f27f2938afd269a691cc078499 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Thu, 16 Jul 2026 12:55:48 -0500 Subject: [PATCH 1/8] Let a mouse-button binding override the built-in select/command --- rts/Game/UI/KeyCodes.cpp | 3 ++- rts/Game/UI/MouseHandler.cpp | 19 +++++++++++++++++++ rts/Game/UI/ScanCodes.cpp | 3 ++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/rts/Game/UI/KeyCodes.cpp b/rts/Game/UI/KeyCodes.cpp index 6169852e453..10d16c24c33 100644 --- a/rts/Game/UI/KeyCodes.cpp +++ b/rts/Game/UI/KeyCodes.cpp @@ -160,7 +160,8 @@ void CKeyCodes::Reset() //AddPair("euro", SDLK_EURO); // Some european keyboards //AddPair("undo", SDLK_UNDO); // Atari keyboard has Undo - for (int i = ACTION_BUTTON_MIN; i <= NUM_BUTTONS; i++) { + // from mouse1 (LMB): it's bindable now, and a binding overrides the built-in + for (int i = 1; i <= NUM_BUTTONS; i++) { AddPair("mouse" + IntToString(i), CKeyCodes::GetMouseButtonSymbol(i)); } diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 400cffa743f..1d72d65494a 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -5,7 +5,10 @@ #include "CommandColors.h" #include "InputReceiver.h" #include "GuiHandler.h" +#include "KeyBindings.h" +#include "KeyCodes.h" #include "MiniMap.h" +#include "ScanCodes.h" #include "MouseCursor.h" #include "TooltipConsole.h" #include "Game/CameraHandler.h" @@ -378,6 +381,22 @@ void CMouseHandler::MousePress(int x, int y, int button) return; } + // A binding on this button overrides the built-in select/command, but only over + // the world - clicks on UI (menu, minimap, widgets) keep their normal handling. + // The check is modifier-aware and doesn't disturb the chord chain, so an unbound + // click behaves exactly as before. A bound click is consumed either way, so the + // built-in never also fires. + if (game != nullptr && !game->hideInterface && CInputReceiver::GetReceiverAt(x, y) == nullptr) { + CInputReceiver* gameReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + + if (gameReceiver != nullptr && + !keyBindings.GetActionList(CKeyCodes::GetMouseButtonSymbol(button), CScanCodes::GetMouseButtonSymbol(button)).empty()) { + gameReceiver->MousePress(x, y, button); + activeReceiver = gameReceiver; + return; + } + } + if (game != nullptr && !game->hideInterface) { for (CInputReceiver* recv: CInputReceiver::GetReceivers()) { if (recv != nullptr && recv->MousePress(x, y, button)) { diff --git a/rts/Game/UI/ScanCodes.cpp b/rts/Game/UI/ScanCodes.cpp index dcf7a01d33c..71f3cebe8e5 100644 --- a/rts/Game/UI/ScanCodes.cpp +++ b/rts/Game/UI/ScanCodes.cpp @@ -214,7 +214,8 @@ void CScanCodes::Reset() AddPair("sc_alt", SDL_SCANCODE_LALT); AddPair("sc_meta", SDL_SCANCODE_LGUI); - for (int i = ACTION_BUTTON_MIN; i <= NUM_BUTTONS; i++) { + // from mouse1 (LMB): it's bindable now, and a binding overrides the built-in + for (int i = 1; i <= NUM_BUTTONS; i++) { AddPair("sc_mouse" + IntToString(i), CScanCodes::GetMouseButtonSymbol(i)); } From 75cd72d7dc50818cde86aef4985253e06c7efe82 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Fri, 17 Jul 2026 09:44:50 -0500 Subject: [PATCH 2/8] Drop AI comments from the mouse-button bind loops --- rts/Game/UI/KeyCodes.cpp | 1 - rts/Game/UI/ScanCodes.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/rts/Game/UI/KeyCodes.cpp b/rts/Game/UI/KeyCodes.cpp index 10d16c24c33..a83ca861e45 100644 --- a/rts/Game/UI/KeyCodes.cpp +++ b/rts/Game/UI/KeyCodes.cpp @@ -160,7 +160,6 @@ void CKeyCodes::Reset() //AddPair("euro", SDLK_EURO); // Some european keyboards //AddPair("undo", SDLK_UNDO); // Atari keyboard has Undo - // from mouse1 (LMB): it's bindable now, and a binding overrides the built-in for (int i = 1; i <= NUM_BUTTONS; i++) { AddPair("mouse" + IntToString(i), CKeyCodes::GetMouseButtonSymbol(i)); } diff --git a/rts/Game/UI/ScanCodes.cpp b/rts/Game/UI/ScanCodes.cpp index 71f3cebe8e5..3354dae989c 100644 --- a/rts/Game/UI/ScanCodes.cpp +++ b/rts/Game/UI/ScanCodes.cpp @@ -214,7 +214,6 @@ void CScanCodes::Reset() AddPair("sc_alt", SDL_SCANCODE_LALT); AddPair("sc_meta", SDL_SCANCODE_LGUI); - // from mouse1 (LMB): it's bindable now, and a binding overrides the built-in for (int i = 1; i <= NUM_BUTTONS; i++) { AddPair("sc_mouse" + IntToString(i), CScanCodes::GetMouseButtonSymbol(i)); } From a25d5d5fbef8643f39c63afa066e935c0f197dca Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Fri, 17 Jul 2026 09:44:50 -0500 Subject: [PATCH 3/8] Extract MousePress consumer checks into named helpers --- rts/Game/UI/MouseHandler.cpp | 89 ++++++++++++++++++++++++------------ rts/Game/UI/MouseHandler.h | 4 ++ 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 1d72d65494a..f970f2edff4 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -335,6 +335,58 @@ void CMouseHandler::ClearEmulatedButtons() } +bool CMouseHandler::ConsumeByLua(int x, int y, int button) +{ + if (!luaInputReceiver->MousePress(x, y, button)) + return false; + + if (activeReceiver == nullptr) + activeReceiver = luaInputReceiver; + + return true; +} + + +bool CMouseHandler::ConsumeByActionBindings(int x, int y, int button) +{ + if (game == nullptr || game->hideInterface) + return false; + + if (CInputReceiver::GetReceiverAt(x, y) != nullptr) + return false; + + CInputReceiver* gameReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + + if (gameReceiver == nullptr) + return false; + + if (keyBindings.GetActionList(CKeyCodes::GetMouseButtonSymbol(button), CScanCodes::GetMouseButtonSymbol(button)).empty()) + return false; + + gameReceiver->MousePress(x, y, button); + activeReceiver = gameReceiver; + return true; +} + + +bool CMouseHandler::ConsumeByInputReceivers(int x, int y, int button) +{ + if (game == nullptr || game->hideInterface) + return false; + + for (CInputReceiver* recv: CInputReceiver::GetReceivers()) { + if (recv != nullptr && recv->MousePress(x, y, button)) { + if (activeReceiver == nullptr) + activeReceiver = recv; + + return true; + } + } + + return false; +} + + void CMouseHandler::MousePress(int x, int y, int button) { RECOIL_DETAILED_TRACY_ZONE; @@ -375,41 +427,18 @@ void CMouseHandler::MousePress(int x, int y, int button) if (button == SDL_BUTTON_MIDDLE && locked) return; - if (luaInputReceiver->MousePress(x, y, button)) { - if (activeReceiver == nullptr) - activeReceiver = luaInputReceiver; + if (ConsumeByLua(x, y, button)) return; - } - // A binding on this button overrides the built-in select/command, but only over - // the world - clicks on UI (menu, minimap, widgets) keep their normal handling. - // The check is modifier-aware and doesn't disturb the chord chain, so an unbound - // click behaves exactly as before. A bound click is consumed either way, so the - // built-in never also fires. - if (game != nullptr && !game->hideInterface && CInputReceiver::GetReceiverAt(x, y) == nullptr) { - CInputReceiver* gameReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); - - if (gameReceiver != nullptr && - !keyBindings.GetActionList(CKeyCodes::GetMouseButtonSymbol(button), CScanCodes::GetMouseButtonSymbol(button)).empty()) { - gameReceiver->MousePress(x, y, button); - activeReceiver = gameReceiver; - return; - } - } - - if (game != nullptr && !game->hideInterface) { - for (CInputReceiver* recv: CInputReceiver::GetReceivers()) { - if (recv != nullptr && recv->MousePress(x, y, button)) { - if (activeReceiver == nullptr) - activeReceiver = recv; - - return; - } - } + if (ConsumeByActionBindings(x, y, button)) + return; - } + if (ConsumeByInputReceivers(x, y, button)) + return; auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + + // mouse1 exclusively handled by consumers above if (button >= ACTION_BUTTON_MIN && activeControllerReceiver && activeControllerReceiver->MousePress(x, y, button)) { activeReceiver = activeControllerReceiver; return; diff --git a/rts/Game/UI/MouseHandler.h b/rts/Game/UI/MouseHandler.h index 0b8074efdb6..465441712c0 100644 --- a/rts/Game/UI/MouseHandler.h +++ b/rts/Game/UI/MouseHandler.h @@ -103,6 +103,10 @@ class CMouseHandler int2 GetViewMouseCenter() const; void SetCursor(const std::string& cmdName, const bool forceRebind = false); + bool ConsumeByLua(int x, int y, int button); + bool ConsumeByActionBindings(int x, int y, int button); + bool ConsumeByInputReceivers(int x, int y, int button); + void DrawScrollCursor(TypedRenderBuffer& rb) const; void DrawFPSCursor(TypedRenderBuffer& rb) const; From 76ef3cd6fcc110f1b8f3cc38a8f80b198e6c2653 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Fri, 17 Jul 2026 15:47:25 -0500 Subject: [PATCH 4/8] Drop the mouse-button MousePress override --- rts/Game/UI/MouseHandler.cpp | 76 +++++++----------------------------- rts/Game/UI/MouseHandler.h | 4 -- 2 files changed, 14 insertions(+), 66 deletions(-) diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index f970f2edff4..400cffa743f 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -5,10 +5,7 @@ #include "CommandColors.h" #include "InputReceiver.h" #include "GuiHandler.h" -#include "KeyBindings.h" -#include "KeyCodes.h" #include "MiniMap.h" -#include "ScanCodes.h" #include "MouseCursor.h" #include "TooltipConsole.h" #include "Game/CameraHandler.h" @@ -335,58 +332,6 @@ void CMouseHandler::ClearEmulatedButtons() } -bool CMouseHandler::ConsumeByLua(int x, int y, int button) -{ - if (!luaInputReceiver->MousePress(x, y, button)) - return false; - - if (activeReceiver == nullptr) - activeReceiver = luaInputReceiver; - - return true; -} - - -bool CMouseHandler::ConsumeByActionBindings(int x, int y, int button) -{ - if (game == nullptr || game->hideInterface) - return false; - - if (CInputReceiver::GetReceiverAt(x, y) != nullptr) - return false; - - CInputReceiver* gameReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); - - if (gameReceiver == nullptr) - return false; - - if (keyBindings.GetActionList(CKeyCodes::GetMouseButtonSymbol(button), CScanCodes::GetMouseButtonSymbol(button)).empty()) - return false; - - gameReceiver->MousePress(x, y, button); - activeReceiver = gameReceiver; - return true; -} - - -bool CMouseHandler::ConsumeByInputReceivers(int x, int y, int button) -{ - if (game == nullptr || game->hideInterface) - return false; - - for (CInputReceiver* recv: CInputReceiver::GetReceivers()) { - if (recv != nullptr && recv->MousePress(x, y, button)) { - if (activeReceiver == nullptr) - activeReceiver = recv; - - return true; - } - } - - return false; -} - - void CMouseHandler::MousePress(int x, int y, int button) { RECOIL_DETAILED_TRACY_ZONE; @@ -427,18 +372,25 @@ void CMouseHandler::MousePress(int x, int y, int button) if (button == SDL_BUTTON_MIDDLE && locked) return; - if (ConsumeByLua(x, y, button)) + if (luaInputReceiver->MousePress(x, y, button)) { + if (activeReceiver == nullptr) + activeReceiver = luaInputReceiver; return; + } - if (ConsumeByActionBindings(x, y, button)) - return; + if (game != nullptr && !game->hideInterface) { + for (CInputReceiver* recv: CInputReceiver::GetReceivers()) { + if (recv != nullptr && recv->MousePress(x, y, button)) { + if (activeReceiver == nullptr) + activeReceiver = recv; - if (ConsumeByInputReceivers(x, y, button)) - return; + return; + } + } - auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + } - // mouse1 exclusively handled by consumers above + auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); if (button >= ACTION_BUTTON_MIN && activeControllerReceiver && activeControllerReceiver->MousePress(x, y, button)) { activeReceiver = activeControllerReceiver; return; diff --git a/rts/Game/UI/MouseHandler.h b/rts/Game/UI/MouseHandler.h index 465441712c0..0b8074efdb6 100644 --- a/rts/Game/UI/MouseHandler.h +++ b/rts/Game/UI/MouseHandler.h @@ -103,10 +103,6 @@ class CMouseHandler int2 GetViewMouseCenter() const; void SetCursor(const std::string& cmdName, const bool forceRebind = false); - bool ConsumeByLua(int x, int y, int button); - bool ConsumeByActionBindings(int x, int y, int button); - bool ConsumeByInputReceivers(int x, int y, int button); - void DrawScrollCursor(TypedRenderBuffer& rb) const; void DrawFPSCursor(TypedRenderBuffer& rb) const; From 01ebfbaa3c1d03ab9e6bdf5d74cb420fa7cd7301 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Fri, 17 Jul 2026 16:30:28 -0500 Subject: [PATCH 5/8] Drive unit select/command off bindable mouse-role actions --- rts/Game/UI/GuiHandler.cpp | 91 ++++++++++++++++++++---------------- rts/Game/UI/KeyBindings.cpp | 3 ++ rts/Game/UI/MouseHandler.cpp | 84 +++++++++++++++++++++++++++++---- rts/Game/UI/MouseHandler.h | 8 +++- 4 files changed, 138 insertions(+), 48 deletions(-) diff --git a/rts/Game/UI/GuiHandler.cpp b/rts/Game/UI/GuiHandler.cpp index 370e21cbe93..2062d1a4f72 100644 --- a/rts/Game/UI/GuiHandler.cpp +++ b/rts/Game/UI/GuiHandler.cpp @@ -582,7 +582,7 @@ void CGuiHandler::LayoutIcons(bool useSelectionPage) SCommandDescription cmdDesc; { defCmd = - (mouse->buttons[SDL_BUTTON_RIGHT].pressed && + (mouse->IsActionButtonPressed("mousesecondary") && (defaultCmdMemory >= 0) && (inCommand < 0) && ((activeReceiver == this) || (minimap->ProxyMode()))); @@ -1124,7 +1124,7 @@ void CGuiHandler::SetCursorIcon() const else if (!useMinimap || minimap->FullProxy()) { int defcmd; - if (mouse->buttons[SDL_BUTTON_RIGHT].pressed && ((activeReceiver == this) || (minimap->ProxyMode()))) { + if (mouse->IsActionButtonPressed("mousesecondary") && ((activeReceiver == this) || (minimap->ProxyMode()))) { defcmd = defaultCmdMemory; } else { defcmd = GetDefaultCommand(mouse->lastx, mouse->lasty); @@ -1205,7 +1205,8 @@ bool CGuiHandler::MousePress(int x, int y, int button) { RECOIL_DETAILED_TRACY_ZONE; { - if (button != SDL_BUTTON_LEFT && button != SDL_BUTTON_RIGHT && button != -SDL_BUTTON_RIGHT && button != -SDL_BUTTON_LEFT) + const int roleButton = (button < 0) ? -button : button; + if (!mouse->IsButtonBoundToAction(roleButton, "mouseprimary") && !mouse->IsButtonBoundToAction(roleButton, "mousesecondary")) return false; if (button < 0) { @@ -1221,7 +1222,7 @@ bool CGuiHandler::MousePress(int x, int y, int button) curIconCommand = icons[iconPos].commandsID; } } - if (button == SDL_BUTTON_RIGHT) + if (mouse->IsButtonBoundToAction(button, "mousesecondary")) SetActiveCommandIndex(defaultCmdMemory = -1); return true; } @@ -1230,8 +1231,8 @@ bool CGuiHandler::MousePress(int x, int y, int button) } if (inCommand >= 0) { - if (invertQueueKey && (button == SDL_BUTTON_RIGHT) && - !mouse->buttons[SDL_BUTTON_LEFT].pressed) { // for rocker gestures + if (invertQueueKey && mouse->IsButtonBoundToAction(button, "mousesecondary") && + !mouse->IsActionButtonPressed("mouseprimary")) { // for rocker gestures SetActiveCommandIndex(-1); needShift = false; return false; @@ -1240,7 +1241,7 @@ bool CGuiHandler::MousePress(int x, int y, int button) return true; } } - if (button == SDL_BUTTON_RIGHT) { + if (mouse->IsButtonBoundToAction(button, "mousesecondary")) { activeMousePress = true; defaultCmdMemory = GetDefaultCommand(x, y); return true; @@ -1253,7 +1254,8 @@ bool CGuiHandler::MousePress(int x, int y, int button) void CGuiHandler::MouseRelease(int x, int y, int button, const float3& cameraPos, const float3& mouseDir) { RECOIL_DETAILED_TRACY_ZONE; - if (button != SDL_BUTTON_LEFT && button != SDL_BUTTON_RIGHT && button != -SDL_BUTTON_RIGHT && button != -SDL_BUTTON_LEFT) + const int roleButton = (button < 0) ? -button : button; + if (!mouse->IsButtonBoundToAction(roleButton, "mouseprimary") && !mouse->IsButtonBoundToAction(roleButton, "mousesecondary")) return; int lastIconCmd = curIconCommand; @@ -1285,14 +1287,14 @@ void CGuiHandler::MouseRelease(int x, int y, int button, const float3& cameraPos } } - if ((button == SDL_BUTTON_RIGHT) && (iconCmd == -1)) { + if (mouse->IsButtonBoundToAction(button, "mousesecondary") && (iconCmd == -1)) { // right click -> set the default cmd SetActiveCommandIndex(defaultCmdMemory); defaultCmdMemory = -1; } if (size_t(iconCmd) < commands.size()) { - SetActiveCommand(iconCmd, button == SDL_BUTTON_RIGHT); + SetActiveCommand(iconCmd, mouse->IsButtonBoundToAction(button, "mousesecondary")); return; } @@ -1417,18 +1419,22 @@ bool CGuiHandler::SetActiveCommand(int cmdIndex, int button, { RECOIL_DETAILED_TRACY_ZONE; // use the button value instead of rightMouseButton - const bool effectiveRMB = (button == SDL_BUTTON_LEFT) ? false : true; + const bool effectiveRMB = !mouse->IsButtonBoundToAction(button, "mouseprimary"); + + // spoof state on whichever buttons carry the select/command roles + const int selectButton = mouse->GetActionButton("mouseprimary"); + const int commandButton = mouse->GetActionButton("mousesecondary"); // setup the mouse and key states - const bool prevLMB = mouse->buttons[SDL_BUTTON_LEFT].pressed; - const bool prevRMB = mouse->buttons[SDL_BUTTON_RIGHT].pressed; + const bool prevLMB = (selectButton > 0) && mouse->buttons[selectButton ].pressed; + const bool prevRMB = (commandButton > 0) && mouse->buttons[commandButton].pressed; const std::uint8_t prevAlt = KeyInput::GetKeyModState(KMOD_ALT); const std::uint8_t prevCtrl = KeyInput::GetKeyModState(KMOD_CTRL); const std::uint8_t prevMeta = KeyInput::GetKeyModState(KMOD_GUI); const std::uint8_t prevShift = KeyInput::GetKeyModState(KMOD_SHIFT); - mouse->buttons[SDL_BUTTON_LEFT].pressed = leftMouseButton; - mouse->buttons[SDL_BUTTON_RIGHT].pressed = rightMouseButton; + if (selectButton > 0) mouse->buttons[selectButton ].pressed = leftMouseButton; + if (commandButton > 0) mouse->buttons[commandButton].pressed = rightMouseButton; KeyInput::SetKeyModState(KMOD_ALT, alt); KeyInput::SetKeyModState(KMOD_CTRL, ctrl); @@ -1443,8 +1449,8 @@ bool CGuiHandler::SetActiveCommand(int cmdIndex, int button, KeyInput::SetKeyModState(KMOD_CTRL, prevCtrl); KeyInput::SetKeyModState(KMOD_ALT, prevAlt); - mouse->buttons[SDL_BUTTON_RIGHT].pressed = prevRMB; - mouse->buttons[SDL_BUTTON_LEFT].pressed = prevLMB; + if (commandButton > 0) mouse->buttons[commandButton].pressed = prevRMB; + if (selectButton > 0) mouse->buttons[selectButton ].pressed = prevLMB; return retval; } @@ -1642,8 +1648,8 @@ unsigned char CGuiHandler::CreateOptions(bool rightMouseButton) // allow mouse button 'rocker' movements to force // immediate mode (when queuing is the default mode) if (!invertQueueKey || - (!mouse->buttons[SDL_BUTTON_LEFT].pressed && - !mouse->buttons[SDL_BUTTON_RIGHT].pressed)) { + (!mouse->IsActionButtonPressed("mouseprimary") && + !mouse->IsActionButtonPressed("mousesecondary"))) { options |= SHIFT_KEY; } } @@ -1656,7 +1662,7 @@ unsigned char CGuiHandler::CreateOptions(bool rightMouseButton) unsigned char CGuiHandler::CreateOptions(int button) { RECOIL_DETAILED_TRACY_ZONE; - return CreateOptions(button != SDL_BUTTON_LEFT); + return CreateOptions(!mouse->IsButtonBoundToAction(button, "mouseprimary")); } @@ -2071,7 +2077,7 @@ bool CGuiHandler::KeyReleased(int keyCode, int scanCode) void CGuiHandler::FinishCommand(int button) { - if ((button == SDL_BUTTON_LEFT) && (KeyInput::GetKeyModState(KMOD_SHIFT) || invertQueueKey)) { + if (mouse->IsButtonBoundToAction(button, "mouseprimary") && (KeyInput::GetKeyModState(KMOD_SHIFT) || invertQueueKey)) { needShift = true; } else { SetActiveCommandIndex(-1); @@ -2177,17 +2183,19 @@ Command CGuiHandler::GetCommand(int mouseX, int mouseY, int buttonHint, bool pre if (buttonHint >= SDL_BUTTON_LEFT) { button = buttonHint; } else if (inCommand != -1) { - button = SDL_BUTTON_LEFT; - } else if (mouse->buttons[SDL_BUTTON_RIGHT].pressed) { - button = SDL_BUTTON_RIGHT; + button = mouse->GetActionButton("mouseprimary"); + if (button < 0) + button = SDL_BUTTON_LEFT; + } else if (mouse->IsActionButtonPressed("mousesecondary")) { + button = mouse->GetPressedActionButton("mousesecondary"); } else { return Command(CMD_STOP); } - if (button == SDL_BUTTON_RIGHT && preview) { + if (mouse->IsButtonBoundToAction(button, "mousesecondary") && preview) { // right click -> default cmd // (in preview we might not have default cmd memory set) - if (mouse->buttons[SDL_BUTTON_RIGHT].pressed) { + if (mouse->IsActionButtonPressed("mousesecondary")) { tempInCommand = defaultCmdMemory; } else { tempInCommand = GetDefaultCommand(mouseX, mouseY, cameraPos, mouseDir); @@ -2208,7 +2216,7 @@ Command CGuiHandler::GetCommand(int mouseX, int mouseY, int buttonHint, bool pre case CMDTYPE_ICON: { Command c(commands[tempInCommand].id, CreateOptions(button)); - if (button == SDL_BUTTON_LEFT && !preview) + if (mouse->IsButtonBoundToAction(button, "mouseprimary") && !preview) LOG_L(L_WARNING, "CMDTYPE_ICON left button press in incommand test? This should not happen."); return CheckCommand(c); @@ -2236,9 +2244,9 @@ Command CGuiHandler::GetCommand(int mouseX, int mouseY, int buttonHint, bool pre const BuildInfo bi(unitdef, cameraPos + mouseDir * dist, buildFacing); - if (GetQueueKeystate() && (button == SDL_BUTTON_LEFT)) { - const float3 camTracePos = mouse->buttons[SDL_BUTTON_LEFT].camPos; - const float3 camTraceDir = mouse->buttons[SDL_BUTTON_LEFT].dir; + if (GetQueueKeystate() && mouse->IsButtonBoundToAction(button, "mouseprimary")) { + const float3 camTracePos = mouse->buttons[button].camPos; + const float3 camTraceDir = mouse->buttons[button].dir; const float traceDist = camera->GetFarPlaneDist() * 1.4f; const float isectDist = CGround::LineGroundWaterCol(camTracePos, camTraceDir, traceDist, unitdef->floatOnWater, false); @@ -3035,7 +3043,7 @@ void CGuiHandler::DrawHilightQuad(const IconInfo& icon) RECOIL_DETAILED_TRACY_ZONE; if (icon.commandsID == inCommand) { glColor4f(0.3f, 0.0f, 0.0f, 1.0f); - } else if (mouse->buttons[SDL_BUTTON_LEFT].pressed) { + } else if (mouse->IsActionButtonPressed("mouseprimary")) { glColor4f(0.2f, 0.0f, 0.0f, 1.0f); } else { glColor4f(0.0f, 0.0f, 0.2f, 1.0f); @@ -3179,8 +3187,8 @@ void CGuiHandler::DrawButtons() // Only called by Draw if (highlight) { if (icon.commandsID == inCommand) { glColor4f(1.0f, 1.0f, 0.0f, 0.75f); - } else if (mouse->buttons[SDL_BUTTON_LEFT].pressed || - mouse->buttons[SDL_BUTTON_RIGHT].pressed) { + } else if (mouse->IsActionButtonPressed("mouseprimary") || + mouse->IsActionButtonPressed("mousesecondary")) { glColor4f(1.0f, 0.0f, 0.0f, 0.50f); } else { glColor4f(1.0f, 1.0f, 1.0f, 0.50f); @@ -3575,15 +3583,19 @@ void CGuiHandler::DrawMapStuff(bool onMiniMap) if (activeMousePress) { int cmdIndex = -1; - int button = SDL_BUTTON_LEFT; + int button = mouse->GetPressedActionButton("mouseprimary"); + if (button < 0) + button = mouse->GetActionButton("mouseprimary"); + if (button < 0) + button = SDL_BUTTON_LEFT; if (size_t(inCommand) < commands.size()) { cmdIndex = inCommand; } else { - if (mouse->buttons[SDL_BUTTON_RIGHT].pressed && + if (mouse->IsActionButtonPressed("mousesecondary") && ((activeReceiver == this) || (minimap->ProxyMode()))) { cmdIndex = defaultCmdMemory; - button = SDL_BUTTON_RIGHT; + button = mouse->GetPressedActionButton("mousesecondary"); } } @@ -3818,8 +3830,9 @@ void CGuiHandler::DrawMapStuff(bool onMiniMap) // get the build information const float3 cPos = tracePos + traceDir * rayTraceDist; - const CMouseHandler::ButtonPressEvt& bp = mouse->buttons[SDL_BUTTON_LEFT]; - if (GetQueueKeystate() && bp.pressed) { + const int selBtn = mouse->GetPressedActionButton("mouseprimary"); + const CMouseHandler::ButtonPressEvt& bp = mouse->buttons[(selBtn > 0) ? selBtn : SDL_BUTTON_LEFT]; + if (GetQueueKeystate() && (selBtn > 0) && bp.pressed) { const float bpDist = CGround::LineGroundWaterCol(bp.camPos, bp.dir, maxTraceDist, buildeeDef->floatOnWater, false); const float3 bPos = bp.camPos + bp.dir * bpDist; const BuildInfo cInfo = BuildInfo(buildeeDef, cPos, buildFacing); @@ -4003,7 +4016,7 @@ void CGuiHandler::DrawCentroidCursor() } else { size_t defcmd = 0; - if (mouse->buttons[SDL_BUTTON_RIGHT].pressed && ((activeReceiver == this) || (minimap->ProxyMode()))) { + if (mouse->IsActionButtonPressed("mousesecondary") && ((activeReceiver == this) || (minimap->ProxyMode()))) { defcmd = defaultCmdMemory; } else { defcmd = GetDefaultCommand(mouse->lastx, mouse->lasty); diff --git a/rts/Game/UI/KeyBindings.cpp b/rts/Game/UI/KeyBindings.cpp index 4dd9e097110..a308285755e 100644 --- a/rts/Game/UI/KeyBindings.cpp +++ b/rts/Game/UI/KeyBindings.cpp @@ -64,6 +64,9 @@ static const DefaultBinding defaultBindings[] = { { "Alt+Shift+esc", "reloadforce" }, { "Any+pause", "pause" }, + { "Any+mouse1", "mouseprimary" }, + { "Any+mouse3", "mousesecondary" }, + { "c", "controlunit" }, { "Any+h", "sharedialog" }, { "Any+i", "gameinfo" }, diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 400cffa743f..7a8d0c49bc6 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -8,6 +8,9 @@ #include "MiniMap.h" #include "MouseCursor.h" #include "TooltipConsole.h" +#include "KeyBindings.h" +#include "KeyCodes.h" +#include "ScanCodes.h" #include "Game/CameraHandler.h" #include "Game/Camera.h" #include "Game/Game.h" @@ -276,8 +279,10 @@ void CMouseHandler::MouseMove(int x, int y, int dx, int dy) } const int movedPixels = (int)fastmath::sqrt_sse(float(dx*dx + dy*dy)); - buttons[SDL_BUTTON_LEFT ].movement += movedPixels; - buttons[SDL_BUTTON_RIGHT].movement += movedPixels; + for (int b = 1; b <= NUM_BUTTONS; ++b) { + if (buttons[b].pressed) + buttons[b].movement += movedPixels; + } if (game != nullptr && !game->IsGameOver()) playerHandler.Player(gu->myPlayerNum)->currentStats.mousePixels += movedPixels; @@ -332,6 +337,62 @@ void CMouseHandler::ClearEmulatedButtons() } +bool CMouseHandler::IsButtonBoundToAction(int button, const std::string& action) const +{ + if (button < 1 || button > NUM_BUTTONS) + return false; + + for (const Action& a: keyBindings.GetActionList(CKeyCodes::GetMouseButtonSymbol(button), CScanCodes::GetMouseButtonSymbol(button))) { + if (a.command == action) + return true; + } + + return false; +} + + +int CMouseHandler::GetPressedActionButton(const std::string& action) const +{ + for (int b = 1; b <= NUM_BUTTONS; ++b) { + if (buttons[b].pressed && IsButtonBoundToAction(b, action)) + return b; + } + + return -1; +} + + +bool CMouseHandler::IsActionButtonPressed(const std::string& action) const +{ + return GetPressedActionButton(action) >= 0; +} + + +int CMouseHandler::GetActionButton(const std::string& action) const +{ + for (int b = 1; b <= NUM_BUTTONS; ++b) { + if (IsButtonBoundToAction(b, action)) + return b; + } + + return -1; +} + + +bool CMouseHandler::IsOtherActionButtonPressed(int button) const +{ + for (int b = 1; b <= NUM_BUTTONS; ++b) { + if (b == button || !buttons[b].pressed) + continue; + + if (IsButtonBoundToAction(b, "mouseprimary") || IsButtonBoundToAction(b, "mousesecondary")) + return true; + } + + return false; +} + + void CMouseHandler::MousePress(int x, int y, int button) { RECOIL_DETAILED_TRACY_ZONE; @@ -350,7 +411,7 @@ void CMouseHandler::MousePress(int x, int y, int button) activeButtonIdx = button; ButtonPressEvt& bp = buttons[activeButtonIdx]; - bp.chorded = (buttons[SDL_BUTTON_LEFT].pressed || buttons[SDL_BUTTON_RIGHT].pressed); + bp.chorded = IsOtherActionButtonPressed(button); bp.pressed = true; bp.time = gu->gameTime; bp.x = x; @@ -391,7 +452,8 @@ void CMouseHandler::MousePress(int x, int y, int button) } auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); - if (button >= ACTION_BUTTON_MIN && activeControllerReceiver && activeControllerReceiver->MousePress(x, y, button)) { + const bool gestureRoleButton = IsButtonBoundToAction(button, "mouseprimary") || IsButtonBoundToAction(button, "mousesecondary"); + if (!gestureRoleButton && activeControllerReceiver && activeControllerReceiver->MousePress(x, y, button)) { activeReceiver = activeControllerReceiver; return; } @@ -427,7 +489,12 @@ bool CMouseHandler::GetSelectionBoxVertices(float3& bl, float3& br, float3& tl, if (inMapDrawer != nullptr && inMapDrawer->IsDrawMode()) return false; - const ButtonPressEvt& bp = buttons[SDL_BUTTON_LEFT]; + const int selectButton = GetPressedActionButton("mouseprimary"); + + if (selectButton < 0) + return false; + + const ButtonPressEvt& bp = buttons[selectButton]; if (!bp.pressed) return false; @@ -539,7 +606,8 @@ void CMouseHandler::MouseRelease(int x, int y, int button) return; } - if (button >= ACTION_BUTTON_MIN && activeController != nullptr && activeController->MouseRelease(x, y, button)) { + const bool gestureRoleButton = IsButtonBoundToAction(button, "mouseprimary") || IsButtonBoundToAction(button, "mousesecondary"); + if (!gestureRoleButton && activeController != nullptr && activeController->MouseRelease(x, y, button)) { return; } @@ -560,8 +628,8 @@ void CMouseHandler::MouseRelease(int x, int y, int button) if (guihandler == nullptr) return; - if ((button == SDL_BUTTON_LEFT) && !buttons[button].chorded) { - ButtonPressEvt& bp = buttons[SDL_BUTTON_LEFT]; + if (IsButtonBoundToAction(button, "mouseprimary") && !buttons[button].chorded) { + ButtonPressEvt& bp = buttons[button]; if (!KeyInput::GetKeyModState(KMOD_SHIFT) && !KeyInput::GetKeyModState(KMOD_CTRL) && selectedUnitsHandler.GetBoxSelectionHandledByEngine()) selectedUnitsHandler.ClearSelected(); diff --git a/rts/Game/UI/MouseHandler.h b/rts/Game/UI/MouseHandler.h index 0b8074efdb6..7e88d49105e 100644 --- a/rts/Game/UI/MouseHandler.h +++ b/rts/Game/UI/MouseHandler.h @@ -13,7 +13,6 @@ #include "MouseCursor.h" static const int NUM_BUTTONS = 10; -static const int ACTION_BUTTON_MIN = 2; class CInputReceiver; class CCameraController; @@ -99,10 +98,17 @@ class CMouseHandler bool ButtonPressed(); + bool IsButtonBoundToAction(int button, const std::string& action) const; + bool IsActionButtonPressed(const std::string& action) const; + int GetActionButton(const std::string& action) const; + int GetPressedActionButton(const std::string& action) const; + private: int2 GetViewMouseCenter() const; void SetCursor(const std::string& cmdName, const bool forceRebind = false); + bool IsOtherActionButtonPressed(int button) const; + void DrawScrollCursor(TypedRenderBuffer& rb) const; void DrawFPSCursor(TypedRenderBuffer& rb) const; From 975562c563553475a727e4d32ddc1b0647012e39 Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Sat, 18 Jul 2026 02:50:25 -0500 Subject: [PATCH 6/8] Replace stale left/right comments and names with role terms --- rts/Game/UI/GuiHandler.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rts/Game/UI/GuiHandler.cpp b/rts/Game/UI/GuiHandler.cpp index 2062d1a4f72..ddaf388303c 100644 --- a/rts/Game/UI/GuiHandler.cpp +++ b/rts/Game/UI/GuiHandler.cpp @@ -1288,7 +1288,7 @@ void CGuiHandler::MouseRelease(int x, int y, int button, const float3& cameraPos } if (mouse->IsButtonBoundToAction(button, "mousesecondary") && (iconCmd == -1)) { - // right click -> set the default cmd + // command-role button -> set the default cmd SetActiveCommandIndex(defaultCmdMemory); defaultCmdMemory = -1; } @@ -1419,15 +1419,15 @@ bool CGuiHandler::SetActiveCommand(int cmdIndex, int button, { RECOIL_DETAILED_TRACY_ZONE; // use the button value instead of rightMouseButton - const bool effectiveRMB = !mouse->IsButtonBoundToAction(button, "mouseprimary"); + const bool effectiveSecondary = !mouse->IsButtonBoundToAction(button, "mouseprimary"); // spoof state on whichever buttons carry the select/command roles const int selectButton = mouse->GetActionButton("mouseprimary"); const int commandButton = mouse->GetActionButton("mousesecondary"); // setup the mouse and key states - const bool prevLMB = (selectButton > 0) && mouse->buttons[selectButton ].pressed; - const bool prevRMB = (commandButton > 0) && mouse->buttons[commandButton].pressed; + const bool prevPrimary = (selectButton > 0) && mouse->buttons[selectButton ].pressed; + const bool prevSecondary = (commandButton > 0) && mouse->buttons[commandButton].pressed; const std::uint8_t prevAlt = KeyInput::GetKeyModState(KMOD_ALT); const std::uint8_t prevCtrl = KeyInput::GetKeyModState(KMOD_CTRL); const std::uint8_t prevMeta = KeyInput::GetKeyModState(KMOD_GUI); @@ -1441,7 +1441,7 @@ bool CGuiHandler::SetActiveCommand(int cmdIndex, int button, KeyInput::SetKeyModState(KMOD_GUI, meta); KeyInput::SetKeyModState(KMOD_SHIFT, shift); - const bool retval = SetActiveCommand(cmdIndex, effectiveRMB); + const bool retval = SetActiveCommand(cmdIndex, effectiveSecondary); // revert the mouse and key states KeyInput::SetKeyModState(KMOD_SHIFT, prevShift); @@ -1449,8 +1449,8 @@ bool CGuiHandler::SetActiveCommand(int cmdIndex, int button, KeyInput::SetKeyModState(KMOD_CTRL, prevCtrl); KeyInput::SetKeyModState(KMOD_ALT, prevAlt); - if (commandButton > 0) mouse->buttons[commandButton].pressed = prevRMB; - if (selectButton > 0) mouse->buttons[selectButton ].pressed = prevLMB; + if (commandButton > 0) mouse->buttons[commandButton].pressed = prevSecondary; + if (selectButton > 0) mouse->buttons[selectButton ].pressed = prevPrimary; return retval; } @@ -2193,7 +2193,7 @@ Command CGuiHandler::GetCommand(int mouseX, int mouseY, int buttonHint, bool pre } if (mouse->IsButtonBoundToAction(button, "mousesecondary") && preview) { - // right click -> default cmd + // command-role button -> default cmd // (in preview we might not have default cmd memory set) if (mouse->IsActionButtonPressed("mousesecondary")) { tempInCommand = defaultCmdMemory; @@ -2217,7 +2217,7 @@ Command CGuiHandler::GetCommand(int mouseX, int mouseY, int buttonHint, bool pre case CMDTYPE_ICON: { Command c(commands[tempInCommand].id, CreateOptions(button)); if (mouse->IsButtonBoundToAction(button, "mouseprimary") && !preview) - LOG_L(L_WARNING, "CMDTYPE_ICON left button press in incommand test? This should not happen."); + LOG_L(L_WARNING, "CMDTYPE_ICON primary-role button press in incommand test? This should not happen."); return CheckCommand(c); } From 01eab76a54e6f351b22d145fabced586ca3b625f Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Sat, 18 Jul 2026 02:52:42 -0500 Subject: [PATCH 7/8] Correct the stale rightMouseButton comment in the command spoof --- rts/Game/UI/GuiHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rts/Game/UI/GuiHandler.cpp b/rts/Game/UI/GuiHandler.cpp index ddaf388303c..e376a1dc46a 100644 --- a/rts/Game/UI/GuiHandler.cpp +++ b/rts/Game/UI/GuiHandler.cpp @@ -1418,7 +1418,7 @@ bool CGuiHandler::SetActiveCommand(int cmdIndex, int button, bool alt, bool ctrl, bool meta, bool shift) { RECOIL_DETAILED_TRACY_ZONE; - // use the button value instead of rightMouseButton + // derive the command flag from the button's role, not the passed rightMouseButton const bool effectiveSecondary = !mouse->IsButtonBoundToAction(button, "mouseprimary"); // spoof state on whichever buttons carry the select/command roles @@ -1427,7 +1427,7 @@ bool CGuiHandler::SetActiveCommand(int cmdIndex, int button, // setup the mouse and key states const bool prevPrimary = (selectButton > 0) && mouse->buttons[selectButton ].pressed; - const bool prevSecondary = (commandButton > 0) && mouse->buttons[commandButton].pressed; + const bool prevSecondary = (commandButton > 0) && mouse->buttons[commandButton].pressed; const std::uint8_t prevAlt = KeyInput::GetKeyModState(KMOD_ALT); const std::uint8_t prevCtrl = KeyInput::GetKeyModState(KMOD_CTRL); const std::uint8_t prevMeta = KeyInput::GetKeyModState(KMOD_GUI); From 47130fb8c3443a7a97714879e75a0f25784dccbc Mon Sep 17 00:00:00 2001 From: Robert Burnham Date: Sat, 18 Jul 2026 18:31:58 -0500 Subject: [PATCH 8/8] Resolve Lua command role, cache role masks, fix receiver clear --- rts/Game/UI/MouseHandler.cpp | 25 ++++++++++++++++++++++++- rts/Game/UI/MouseHandler.h | 7 +++++++ rts/Lua/LuaUnsyncedCtrl.cpp | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 7a8d0c49bc6..67565d742d0 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -342,6 +342,11 @@ bool CMouseHandler::IsButtonBoundToAction(int button, const std::string& action) if (button < 1 || button > NUM_BUTTONS) return false; + if (action == "mouseprimary") + return (primaryButtonMask >> button) & 1u; + if (action == "mousesecondary") + return (secondaryButtonMask >> button) & 1u; + for (const Action& a: keyBindings.GetActionList(CKeyCodes::GetMouseButtonSymbol(button), CScanCodes::GetMouseButtonSymbol(button))) { if (a.command == action) return true; @@ -351,6 +356,22 @@ bool CMouseHandler::IsButtonBoundToAction(int button, const std::string& action) } +void CMouseHandler::RefreshRoleButtonMasks() +{ + primaryButtonMask = 0; + secondaryButtonMask = 0; + + for (int button = 1; button <= NUM_BUTTONS; ++button) { + for (const Action& a: keyBindings.GetActionList(CKeyCodes::GetMouseButtonSymbol(button), CScanCodes::GetMouseButtonSymbol(button))) { + if (a.command == "mouseprimary") + primaryButtonMask |= (1u << button); + else if (a.command == "mousesecondary") + secondaryButtonMask |= (1u << button); + } + } +} + + int CMouseHandler::GetPressedActionButton(const std::string& action) const { for (int b = 1; b <= NUM_BUTTONS; ++b) { @@ -600,7 +621,7 @@ void CMouseHandler::MouseRelease(int x, int y, int button) if (activeReceiver != nullptr) { activeReceiver->MouseRelease(x, y, button); - if (!buttons[SDL_BUTTON_LEFT].pressed && !buttons[SDL_BUTTON_MIDDLE].pressed && !buttons[SDL_BUTTON_RIGHT].pressed) + if (!ButtonPressed()) activeReceiver = nullptr; return; @@ -818,6 +839,8 @@ std::string CMouseHandler::GetCurrentTooltip() const void CMouseHandler::Update() { RECOIL_DETAILED_TRACY_ZONE; + RefreshRoleButtonMasks(); + // Rml is very polite about asking for changes to the cursor // so let's make sure it's not ignored! if (RmlGui::IsMouseInteractingWith()) diff --git a/rts/Game/UI/MouseHandler.h b/rts/Game/UI/MouseHandler.h index 7e88d49105e..9a70ab1868e 100644 --- a/rts/Game/UI/MouseHandler.h +++ b/rts/Game/UI/MouseHandler.h @@ -109,6 +109,10 @@ class CMouseHandler bool IsOtherActionButtonPressed(int button) const; + // the draw/command paths query the mouse-role binds every frame; cache which + // buttons carry each role so those queries are a bitmask test, not a keybind lookup + void RefreshRoleButtonMasks(); + void DrawScrollCursor(TypedRenderBuffer& rb) const; void DrawFPSCursor(TypedRenderBuffer& rb) const; @@ -137,6 +141,9 @@ class CMouseHandler uint32_t pressedBitMask = 0; private: + uint32_t primaryButtonMask = 0; + uint32_t secondaryButtonMask = 0; + bool hideCursor = true; bool hwHideCursor = true; bool hardwareCursor = false; diff --git a/rts/Lua/LuaUnsyncedCtrl.cpp b/rts/Lua/LuaUnsyncedCtrl.cpp index 2079d9724ce..6a11c92e4ad 100644 --- a/rts/Lua/LuaUnsyncedCtrl.cpp +++ b/rts/Lua/LuaUnsyncedCtrl.cpp @@ -2817,7 +2817,7 @@ static int SetActiveCommandByIndex(lua_State* L) const int button = luaL_optint(L, 2, 1); // LMB if (args <= 2) { - lua_pushboolean(L, guihandler->SetActiveCommand(cmdIndex, button != SDL_BUTTON_LEFT)); + lua_pushboolean(L, guihandler->SetActiveCommand(cmdIndex, !mouse->IsButtonBoundToAction(button, "mouseprimary"))); return 1; }